From 3d651103839c6826118fb7a44e1091c076ec95f7 Mon Sep 17 00:00:00 2001 From: "zia.fazal@arbisoft.com" Date: Wed, 8 Jul 2020 22:13:03 +0500 Subject: [PATCH] Removed boy choy acceptance tests Removed commented code Fixed broken tests and quality violations instructor dashboard test fixes Fixed pep8 quality violation Removed few remaining non a11y tests Fixed quality violations removed edxapp_acceptance setup file --- common/test/acceptance/fixtures/edxnotes.py | 82 - common/test/acceptance/fixtures/xqueue.py | 51 - common/test/acceptance/pages/common/paging.py | 71 - common/test/acceptance/pages/common/utils.py | 100 +- common/test/acceptance/pages/lms/admin.py | 82 - common/test/acceptance/pages/lms/bookmarks.py | 60 - .../acceptance/pages/lms/certificate_page.py | 61 - .../test/acceptance/pages/lms/completion.py | 26 - .../test/acceptance/pages/lms/course_about.py | 37 - .../test/acceptance/pages/lms/course_home.py | 318 --- .../test/acceptance/pages/lms/course_info.py | 31 - .../test/acceptance/pages/lms/courseware.py | 667 +----- .../acceptance/pages/lms/courseware_search.py | 49 - .../test/acceptance/pages/lms/create_mode.py | 78 - common/test/acceptance/pages/lms/dashboard.py | 217 -- common/test/acceptance/pages/lms/discovery.py | 66 - .../test/acceptance/pages/lms/discussion.py | 643 +----- common/test/acceptance/pages/lms/edxnotes.py | 547 ----- common/test/acceptance/pages/lms/index.py | 53 - .../pages/lms/instructor_dashboard.py | 920 +------- common/test/acceptance/pages/lms/library.py | 50 - common/test/acceptance/pages/lms/login.py | 54 - .../pages/lms/login_and_register.py | 363 --- .../acceptance/pages/lms/pay_and_verify.py | 113 - common/test/acceptance/pages/lms/problem.py | 595 +---- common/test/acceptance/pages/lms/progress.py | 152 +- .../test/acceptance/pages/lms/staff_view.py | 98 - common/test/acceptance/pages/lms/tab_nav.py | 29 +- common/test/acceptance/pages/lms/teams.py | 602 ----- .../acceptance/pages/lms/track_selection.py | 60 - .../test/acceptance/pages/lms/video/video.py | 739 ------ .../acceptance/pages/studio/asset_index.py | 378 ---- .../acceptance/pages/studio/checklists.py | 19 - .../acceptance/pages/studio/course_info.py | 145 -- .../studio/discussion_component_editor.py | 35 - .../test/acceptance/pages/studio/edit_tabs.py | 162 -- .../pages/studio/html_component_editor.py | 289 --- .../acceptance/pages/studio/import_export.py | 341 --- common/test/acceptance/pages/studio/index.py | 389 ---- .../test/acceptance/pages/studio/library.py | 206 +- common/test/acceptance/pages/studio/login.py | 61 - .../acceptance/pages/studio/move_xblock.py | 74 - .../test/acceptance/pages/studio/overview.py | 816 ------- .../acceptance/pages/studio/problem_editor.py | 141 -- .../test/acceptance/pages/studio/settings.py | 327 --- .../pages/studio/settings_advanced.py | 273 --- .../pages/studio/settings_certificates.py | 655 ------ .../pages/studio/settings_graders.py | 365 --- .../studio/settings_group_configurations.py | 358 --- common/test/acceptance/pages/studio/signup.py | 43 - .../pages/studio/textbook_upload.py | 150 -- common/test/acceptance/pages/studio/users.py | 247 -- common/test/acceptance/pages/studio/utils.py | 189 -- .../acceptance/pages/studio/xblock_editor.py | 197 -- .../test/acceptance/pages/xblock/__init__.py | 0 common/test/acceptance/pages/xblock/acid.py | 100 - common/test/acceptance/pages/xblock/utils.py | 18 - common/test/acceptance/setup.py | 42 - .../acceptance/tests/discussion/helpers.py | 46 +- .../discussion/test_cohort_management.py | 798 +------ .../tests/discussion/test_cohorts.py | 157 -- .../tests/discussion/test_discussion.py | 1112 +-------- .../discussion/test_discussion_management.py | 495 ---- common/test/acceptance/tests/helpers.py | 475 ---- .../tests/lms/test_account_settings.py | 461 +--- .../tests/lms/test_certificate_web_view.py | 92 - .../tests/lms/test_learner_profile.py | 79 - .../test/acceptance/tests/lms/test_library.py | 284 --- common/test/acceptance/tests/lms/test_lms.py | 664 +----- .../tests/lms/test_lms_acid_xblock.py | 164 -- .../test_lms_cohorted_courseware_search.py | 271 --- .../tests/lms/test_lms_course_discovery.py | 74 - .../tests/lms/test_lms_course_home.py | 7 - .../tests/lms/test_lms_courseware.py | 646 ------ .../tests/lms/test_lms_courseware_search.py | 199 -- .../tests/lms/test_lms_dashboard.py | 361 +-- .../tests/lms/test_lms_entrance_exams.py | 86 - .../acceptance/tests/lms/test_lms_gating.py | 279 --- .../acceptance/tests/lms/test_lms_help.py | 90 - .../acceptance/tests/lms/test_lms_index.py | 60 - .../lms/test_lms_instructor_dashboard.py | 1045 +-------- .../test/acceptance/tests/lms/test_lms_lti.py | 419 ---- .../acceptance/tests/lms/test_lms_problems.py | 999 +-------- .../test_lms_split_test_courseware_search.py | 122 - .../tests/lms/test_lms_user_preview.py | 145 -- .../tests/lms/test_problem_types.py | 915 +------- .../acceptance/tests/lms/test_programs.py | 24 - .../tests/lms/test_progress_page.py | 154 -- .../test/acceptance/tests/lms/test_teams.py | 1987 ----------------- .../tests/lms/test_unicode_username_admin.py | 55 - .../tests/studio/test_import_export.py | 220 -- .../tests/studio/test_studio_acid_xblock.py | 208 -- .../tests/studio/test_studio_asset.py | 250 --- .../tests/studio/test_studio_bad_data.py | 107 - .../tests/studio/test_studio_components.py | 190 -- .../tests/studio/test_studio_container.py | 1481 ------------ .../test_studio_discussion_component.py | 74 - .../tests/studio/test_studio_general.py | 268 --- .../tests/studio/test_studio_grading.py | 248 -- .../tests/studio/test_studio_home.py | 85 - .../tests/studio/test_studio_html_editor.py | 399 ---- .../tests/studio/test_studio_library.py | 139 -- .../studio/test_studio_library_container.py | 176 -- .../tests/studio/test_studio_outline.py | 1983 ---------------- .../studio/test_studio_problem_editor.py | 123 - .../tests/studio/test_studio_settings.py | 558 ----- .../test_studio_settings_certificates.py | 312 --- .../studio/test_studio_settings_details.py | 219 -- .../tests/test_cohorted_courseware.py | 261 --- .../tests/video/test_studio_video_editor.py | 466 ---- .../tests/video/test_studio_video_module.py | 371 --- .../video/test_studio_video_transcript.py | 445 ---- .../tests/video/test_video_events.py | 189 -- .../tests/video/test_video_module.py | 890 -------- .../tests/video/test_video_times.py | 79 - 115 files changed, 67 insertions(+), 35473 deletions(-) delete mode 100644 common/test/acceptance/fixtures/edxnotes.py delete mode 100644 common/test/acceptance/fixtures/xqueue.py delete mode 100644 common/test/acceptance/pages/common/paging.py delete mode 100644 common/test/acceptance/pages/lms/admin.py delete mode 100644 common/test/acceptance/pages/lms/bookmarks.py delete mode 100644 common/test/acceptance/pages/lms/certificate_page.py delete mode 100644 common/test/acceptance/pages/lms/completion.py delete mode 100644 common/test/acceptance/pages/lms/course_about.py delete mode 100644 common/test/acceptance/pages/lms/course_info.py delete mode 100644 common/test/acceptance/pages/lms/courseware_search.py delete mode 100644 common/test/acceptance/pages/lms/create_mode.py delete mode 100644 common/test/acceptance/pages/lms/discovery.py delete mode 100644 common/test/acceptance/pages/lms/edxnotes.py delete mode 100644 common/test/acceptance/pages/lms/index.py delete mode 100644 common/test/acceptance/pages/lms/library.py delete mode 100644 common/test/acceptance/pages/lms/login.py delete mode 100644 common/test/acceptance/pages/lms/login_and_register.py delete mode 100644 common/test/acceptance/pages/lms/pay_and_verify.py delete mode 100644 common/test/acceptance/pages/lms/teams.py delete mode 100644 common/test/acceptance/pages/lms/track_selection.py delete mode 100644 common/test/acceptance/pages/studio/asset_index.py delete mode 100644 common/test/acceptance/pages/studio/checklists.py delete mode 100644 common/test/acceptance/pages/studio/course_info.py delete mode 100644 common/test/acceptance/pages/studio/discussion_component_editor.py delete mode 100644 common/test/acceptance/pages/studio/edit_tabs.py delete mode 100644 common/test/acceptance/pages/studio/html_component_editor.py delete mode 100644 common/test/acceptance/pages/studio/import_export.py delete mode 100644 common/test/acceptance/pages/studio/index.py delete mode 100644 common/test/acceptance/pages/studio/login.py delete mode 100644 common/test/acceptance/pages/studio/move_xblock.py delete mode 100644 common/test/acceptance/pages/studio/problem_editor.py delete mode 100644 common/test/acceptance/pages/studio/settings_advanced.py delete mode 100644 common/test/acceptance/pages/studio/settings_certificates.py delete mode 100644 common/test/acceptance/pages/studio/settings_graders.py delete mode 100644 common/test/acceptance/pages/studio/settings_group_configurations.py delete mode 100644 common/test/acceptance/pages/studio/signup.py delete mode 100644 common/test/acceptance/pages/studio/textbook_upload.py delete mode 100644 common/test/acceptance/pages/studio/xblock_editor.py delete mode 100644 common/test/acceptance/pages/xblock/__init__.py delete mode 100644 common/test/acceptance/pages/xblock/acid.py delete mode 100644 common/test/acceptance/pages/xblock/utils.py delete mode 100644 common/test/acceptance/setup.py delete mode 100644 common/test/acceptance/tests/discussion/test_cohorts.py delete mode 100644 common/test/acceptance/tests/discussion/test_discussion_management.py delete mode 100644 common/test/acceptance/tests/lms/test_certificate_web_view.py delete mode 100644 common/test/acceptance/tests/lms/test_library.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_acid_xblock.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_cohorted_courseware_search.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_course_discovery.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_courseware.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_courseware_search.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_entrance_exams.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_gating.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_help.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_index.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_lti.py delete mode 100644 common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py delete mode 100644 common/test/acceptance/tests/lms/test_teams.py delete mode 100644 common/test/acceptance/tests/lms/test_unicode_username_admin.py delete mode 100644 common/test/acceptance/tests/studio/test_import_export.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_acid_xblock.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_asset.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_bad_data.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_components.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_container.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_discussion_component.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_general.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_grading.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_home.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_html_editor.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_library_container.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_outline.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_problem_editor.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_settings_certificates.py delete mode 100644 common/test/acceptance/tests/studio/test_studio_settings_details.py delete mode 100644 common/test/acceptance/tests/test_cohorted_courseware.py delete mode 100644 common/test/acceptance/tests/video/test_studio_video_editor.py delete mode 100644 common/test/acceptance/tests/video/test_studio_video_module.py delete mode 100644 common/test/acceptance/tests/video/test_studio_video_transcript.py delete mode 100644 common/test/acceptance/tests/video/test_video_events.py delete mode 100644 common/test/acceptance/tests/video/test_video_times.py diff --git a/common/test/acceptance/fixtures/edxnotes.py b/common/test/acceptance/fixtures/edxnotes.py deleted file mode 100644 index 1aae711ed6..0000000000 --- a/common/test/acceptance/fixtures/edxnotes.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Tools for creating edxnotes content fixture data. -""" - - -import json - -import factory -import requests - -from common.test.acceptance.fixtures import EDXNOTES_STUB_URL - - -class Range(factory.Factory): - class Meta(object): - model = dict - - start = "/div[1]/p[1]" - end = "/div[1]/p[1]" - startOffset = 0 - endOffset = 8 - - -class Note(factory.Factory): - class Meta(object): - model = dict - - user = "dummy-user" - usage_id = "dummy-usage-id" - course_id = "dummy-course-id" - text = "dummy note text" - quote = "dummy note quote" - ranges = [Range()] - - -class EdxNotesFixtureError(Exception): - """ - Error occurred while installing a edxnote fixture. - """ - pass - - -class EdxNotesFixture(object): - notes = [] - - def create_notes(self, notes_list): - self.notes = notes_list - return self - - def install(self): - """ - Push the data to the stub EdxNotes service. - """ - response = requests.post( - '{}/create_notes'.format(EDXNOTES_STUB_URL), - data=json.dumps(self.notes) - ) - - if not response.ok: - raise EdxNotesFixtureError( - u"Could not create notes {0}. Status was {1}".format( - json.dumps(self.notes), response.status_code - ) - ) - - return self - - def cleanup(self): - """ - Cleanup the stub EdxNotes service. - """ - self.notes = [] - response = requests.put('{}/cleanup'.format(EDXNOTES_STUB_URL)) - - if not response.ok: - raise EdxNotesFixtureError( - u"Could not cleanup EdxNotes service {0}. Status was {1}".format( - json.dumps(self.notes), response.status_code - ) - ) - - return self diff --git a/common/test/acceptance/fixtures/xqueue.py b/common/test/acceptance/fixtures/xqueue.py deleted file mode 100644 index 930fb1f93c..0000000000 --- a/common/test/acceptance/fixtures/xqueue.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Fixture to configure XQueue response. -""" - - -import json - -import requests - -from common.test.acceptance.fixtures import XQUEUE_STUB_URL - - -class XQueueResponseFixtureError(Exception): - """ - Error occurred while configuring the stub XQueue. - """ - pass - - -class XQueueResponseFixture(object): - """ - Configure the XQueue stub's response to submissions. - """ - - def __init__(self, pattern, response_dict): - """ - Configure XQueue stub to POST `response_dict` (a dictionary) - back to the LMS when it receives a submission that contains the string - `pattern`. - - Remember that there is one XQueue stub shared by all the tests; - if possible, you should have tests use unique queue names - to avoid conflict between tests running in parallel. - """ - self._pattern = pattern - self._response_dict = response_dict - - def install(self): - """ - Configure the stub via HTTP. - """ - url = XQUEUE_STUB_URL + "/set_config" - - # Configure the stub to respond to submissions to our queue - payload = {self._pattern: json.dumps(self._response_dict)} - response = requests.put(url, data=payload) - - if not response.ok: - raise XQueueResponseFixtureError( - u"Could not configure XQueue stub for queue '{1}'. Status code: {2}".format( - self._pattern, self._response_dict)) diff --git a/common/test/acceptance/pages/common/paging.py b/common/test/acceptance/pages/common/paging.py deleted file mode 100644 index 40bac9cea6..0000000000 --- a/common/test/acceptance/pages/common/paging.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Common mixin for paginated UIs. -""" - - -import six -from selenium.webdriver.common.keys import Keys - - -class PaginatedUIMixin(object): - """Common methods used for paginated UI.""" - - PAGINATION_FOOTER_CSS = 'nav.bottom' - PAGE_NUMBER_INPUT_CSS = 'input#page-number-input' - NEXT_PAGE_BUTTON_CSS = 'button.next-page-link' - PREVIOUS_PAGE_BUTTON_CSS = 'button.previous-page-link' - PAGINATION_HEADER_TEXT_CSS = 'div.search-tools' - CURRENT_PAGE_NUMBER_CSS = 'span.current-page' - TOTAL_PAGES_CSS = 'span.total-pages' - - def get_pagination_header_text(self): - """Return the text showing which items the user is currently viewing.""" - return self.q(css=self.PAGINATION_HEADER_TEXT_CSS).text[0] - - def pagination_controls_visible(self): - """Return true if the pagination controls in the footer are visible.""" - footer_nav = self.q(css=self.PAGINATION_FOOTER_CSS).results[0] - # The footer element itself is non-generic, so check above it - footer_el = footer_nav.find_element_by_xpath('..') - return 'hidden' not in footer_el.get_attribute('class').split() - - def get_current_page_number(self): - """Return the the current page number.""" - return int(self.q(css=self.CURRENT_PAGE_NUMBER_CSS).text[0]) - - @property - def get_total_pages(self): - """Returns the total page value""" - return int(self.q(css=self.TOTAL_PAGES_CSS).text[0]) - - def go_to_page(self, page_number): - """Go to the given page_number in the paginated list results.""" - self.q(css=self.PAGE_NUMBER_INPUT_CSS).results[0].send_keys(six.text_type(page_number), Keys.ENTER) - self.wait_for_ajax() - - def press_next_page_button(self): - """Press the next page button in the paginated list results.""" - self.q(css=self.NEXT_PAGE_BUTTON_CSS).click() - self.wait_for_ajax() - - def press_previous_page_button(self): - """Press the previous page button in the paginated list results.""" - self.q(css=self.PREVIOUS_PAGE_BUTTON_CSS).click() - self.wait_for_ajax() - - def is_next_page_button_enabled(self): - """Return whether the 'next page' button can be clicked.""" - return self.is_enabled(self.NEXT_PAGE_BUTTON_CSS) - - def is_previous_page_button_enabled(self): - """Return whether the 'previous page' button can be clicked.""" - return self.is_enabled(self.PREVIOUS_PAGE_BUTTON_CSS) - - def is_enabled(self, css): - """Return whether the given element is not disabled.""" - return 'is-disabled' not in self.q(css=css).attrs('class')[0] - - @property - def footer_visible(self): - """ Return True if footer is visible else False""" - return self.q(css='.pagination.bottom').visible diff --git a/common/test/acceptance/pages/common/utils.py b/common/test/acceptance/pages/common/utils.py index 6fcd0a6b29..beff6a78a1 100644 --- a/common/test/acceptance/pages/common/utils.py +++ b/common/test/acceptance/pages/common/utils.py @@ -4,63 +4,11 @@ Utility methods common to Studio and the LMS. import six -from bok_choy.promise import BrokenPromise -from selenium.webdriver.common.action_chains import ActionChains -from common.test.acceptance.pages.lms.create_mode import ModeCreationPage -from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage from common.test.acceptance.tests.helpers import disable_animations -def sync_on_notification(page, style='default', wait_for_hide=False): - """ - 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 - """ - hiding_class = 'is-hiding' - shown_class = 'is-shown' - - 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 - - # 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): +def click_css(page, css, source_index=0): """ Click the button/link with the given css and index on the specified page (subclass of PageObject). @@ -80,9 +28,6 @@ def click_css(page, css, source_index=0, require_notification=True): # Click on the element in the browser page.q(css=css).filter(_is_visible).nth(source_index).click() - if require_notification: - sync_on_notification(page) - # Some buttons trigger ajax posts # (e.g. .add-missing-groups-button as configured in split_test_author_view.js) # so after you click anything wait for the ajax call to finish @@ -99,46 +44,3 @@ def confirm_prompt(page, cancel=False, require_notification=None): page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible') require_notification = (not cancel) if require_notification is None else require_notification click_css(page, confirmation_button_css, require_notification=require_notification) - - -def hover(browser, element): - """ - Hover over an element. - """ - ActionChains(browser).move_to_element(element).perform() - - -def enroll_user_track(browser, course_id, track): - """ - Utility method to enroll a user in the audit or verified user track. Creates and connects to the - necessary pages. Selects the track and handles payment for verified. - Supported tracks are 'verified' or 'audit'. - """ - track_selection = TrackSelectionPage(browser, course_id) - - # Select track - track_selection.visit() - track_selection.enroll(track) - - -def add_enrollment_course_modes(browser, course_id, tracks): - """ - Add the specified array of tracks to the given course. - Supported tracks are `verified` and `audit` (all others will be ignored), - and display names assigned are `Verified` and `Audit`, respectively. - """ - for track in tracks: - if track == 'audit': - # Add an audit mode to the course - ModeCreationPage( - browser, - course_id, mode_slug='audit', - mode_display_name='Audit' - ).visit() - - elif track == 'verified': - # Add a verified mode to the course - ModeCreationPage( - browser, course_id, mode_slug='verified', - mode_display_name='Verified', min_price=10 - ).visit() diff --git a/common/test/acceptance/pages/lms/admin.py b/common/test/acceptance/pages/lms/admin.py deleted file mode 100644 index 33f7f6d0e4..0000000000 --- a/common/test/acceptance/pages/lms/admin.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Pages object for the Django's /admin/ views. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL - - -class ChangeUserAdminPage(PageObject): - """ - Change user page in Django's admin. - """ - def __init__(self, browser, user_pk): - super(ChangeUserAdminPage, self).__init__(browser) - self.user_pk = user_pk - - @property - def url(self): - """ - Returns the page URL for the page based on self.user_pk. - """ - - return u'{base}/admin/auth/user/{user_pk}/'.format( - base=BASE_URL, - user_pk=self.user_pk, - ) - - @property - def username(self): - """ - Reads the read-only username. - """ - return self.q(css='.field-username .readonly').text[0] - - @property - def first_name_element(self): - """ - Selects the first name element. - """ - return self.q(css='[name="first_name"]') - - @property - def first_name(self): - """ - Reads the first name value from the input field. - """ - return self.first_name_element.attrs('value')[0] - - @property - def submit_element(self): - """ - Gets the "Save" submit element. - - Note that there are multiple submit elements in the change view. - """ - return self.q(css='input.default[type="submit"]') - - def submit(self): - """ - Submits the form. - """ - self.submit_element.click() - - def change_first_name(self, first_name): - """ - Changes the first name and submits the form. - - Args: - first_name: The first name as unicode. - - """ - - self.first_name_element.fill(first_name) - self.submit() - - def is_browser_on_page(self): - """ - Returns True if the browser is currently on the right page. - """ - return self.q(css='#user_form').present diff --git a/common/test/acceptance/pages/lms/bookmarks.py b/common/test/acceptance/pages/lms/bookmarks.py deleted file mode 100644 index d57872c49b..0000000000 --- a/common/test/acceptance/pages/lms/bookmarks.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Courseware Boomarks -""" - - -from common.test.acceptance.pages.common.paging import PaginatedUIMixin -from common.test.acceptance.pages.lms.course_page import CoursePage - - -class BookmarksPage(CoursePage, PaginatedUIMixin): - """ - Courseware Bookmarks Page. - """ - url_path = "bookmarks" - BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button' - BOOKMARKS_ELEMENT_SELECTOR = '#my-bookmarks' - BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item' - BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail' - - def is_browser_on_page(self): - """ Verify if we are on correct page """ - return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present - - def bookmarks_button_visible(self): - """ Check if bookmarks button is visible """ - return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible - - def results_present(self): - """ Check if bookmarks results are present """ - return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present - - def results_header_text(self): - """ Returns the bookmarks results header text """ - return self.q(css='.bookmarks-results-header').text[0] - - def empty_header_text(self): - """ Returns the bookmarks empty header text """ - return self.q(css='.bookmarks-empty-header').text[0] - - def empty_list_text(self): - """ Returns the bookmarks empty list text """ - return self.q(css='.bookmarks-empty-detail-title').text[0] - - def count(self): - """ Returns the total number of bookmarks in the list """ - return len(self.q(css=self.BOOKMARKED_ITEMS_SELECTOR).results) - - def breadcrumbs(self): - """ Return list of breadcrumbs for all bookmarks """ - breadcrumbs = self.q(css=self.BOOKMARKED_BREADCRUMBS).text - return [breadcrumb.replace('\n', '').split('-') for breadcrumb in breadcrumbs] - - def click_bookmarked_block(self, index): - """ - Click on bookmarked block at index `index` - - Arguments: - index (int): bookmark index in the list - """ - self.q(css=self.BOOKMARKED_ITEMS_SELECTOR).nth(index).click() diff --git a/common/test/acceptance/pages/lms/certificate_page.py b/common/test/acceptance/pages/lms/certificate_page.py deleted file mode 100644 index 831f0e80d9..0000000000 --- a/common/test/acceptance/pages/lms/certificate_page.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Module for Certificates pages. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL - - -class CertificatePage(PageObject): - """ - Certificate web view page. - """ - - url_path = "certificates" - - def __init__(self, browser, user_id, course_id): - """Initialize the page. - - Arguments: - browser (Browser): The browser instance. - user_id: id of the user whom certificate is awarded - course_id: course key of the course where certificate is awarded - """ - super(CertificatePage, self).__init__(browser) - self.user_id = user_id - self.course_id = course_id - - def is_browser_on_page(self): - """ Checks if certificate web view page is being viewed """ - return self.q(css='section.about-accomplishments').present - - @property - def url(self): - """ - Construct a URL to the page - """ - return BASE_URL + "/" + self.url_path + "/user/" + self.user_id + "/course/" + self.course_id - - @property - def accomplishment_banner(self): - """ - returns accomplishment banner. - """ - return self.q(css='section.banner-user') - - @property - def add_to_linkedin_profile_button(self): - """ - returns add to LinkedIn profile button - """ - return self.q(css='button.action-linkedin-profile') - - @property - def add_to_facebook_profile_button(self): - """ - returns Facebook share button - """ - return self.q(css='button.action-share-facebook') diff --git a/common/test/acceptance/pages/lms/completion.py b/common/test/acceptance/pages/lms/completion.py deleted file mode 100644 index 0a09dc3648..0000000000 --- a/common/test/acceptance/pages/lms/completion.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Mixins for completion. -""" - - -class CompletionOnViewMixin(object): - """ - Methods for testing completion on view. - """ - - def xblock_components_mark_completed_on_view_value(self): - """ - Return the xblock components data-mark-completed-on-view-after-delay value. - """ - return self.q(css=self.xblock_component_selector).attrs('data-mark-completed-on-view-after-delay') - - def wait_for_xblock_component_to_be_marked_completed_on_view(self, index=0): - """ - Wait for xblock component to be marked completed on view. - - Arguments - index (int): index of block to wait on. (default is 0) - """ - self.wait_for(lambda: (self.xblock_components_mark_completed_on_view_value()[index] == '0'), - 'Waiting for xblock to be marked completed on view.') diff --git a/common/test/acceptance/pages/lms/course_about.py b/common/test/acceptance/pages/lms/course_about.py deleted file mode 100644 index b738a259f0..0000000000 --- a/common/test/acceptance/pages/lms/course_about.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Course about page (with registration button) -""" - - -from common.test.acceptance.pages.lms.course_page import CoursePage -from common.test.acceptance.pages.lms.login_and_register import RegisterPage - - -class CourseAboutPage(CoursePage): - """ - Course about page (with registration button) - """ - - url_path = "about" - - def is_browser_on_page(self): - return self.q(css='section.course-info').present - - def register(self): - """ - Navigate to the registration page. - Waits for the registration page to load, then - returns the registration page object. - """ - self.q(css='a.register').first.click() - - registration_page = RegisterPage(self.browser, self.course_id) - registration_page.wait_for_page() - return registration_page - - def enroll_in_course(self): - """ - Click on enroll button - """ - self.wait_for_element_visibility('.register', 'Enroll button is present') - self.q(css='.register').first.click() diff --git a/common/test/acceptance/pages/lms/course_home.py b/common/test/acceptance/pages/lms/course_home.py index beac645250..08121cd057 100644 --- a/common/test/acceptance/pages/lms/course_home.py +++ b/common/test/acceptance/pages/lms/course_home.py @@ -3,15 +3,7 @@ LMS Course Home page object """ -from collections import OrderedDict - -from bok_choy.page_object import PageObject -from bok_choy.promise import BrokenPromise -from six import text_type - -from .bookmarks import BookmarksPage from .course_page import CoursePage -from .courseware import CoursewarePage from .staff_view import StaffPreviewPage @@ -30,316 +22,6 @@ class CourseHomePage(CoursePage): def __init__(self, browser, course_id): super(CourseHomePage, self).__init__(browser, course_id) self.course_id = course_id - self.outline = CourseOutlinePage(browser, self) self.preview = StaffPreviewPage(browser, self) # TODO: TNL-6546: Remove the following self.course_outline_page = False - - def select_course_goal(self): - """ Click on a course goal in a message """ - self.q(css='button.goal-option').first.click() - self.wait_for_ajax() - - def is_course_goal_success_message_shown(self): - """ Verifies course goal success message appears. """ - return self.q(css='.success-message').present - - def is_course_goal_update_field_shown(self): - """ Verifies course goal success message appears. """ - return self.q(css='.current-goal-container').visible - - def is_course_goal_update_icon_shown(self, valid=True): - """ Verifies course goal success or error icon appears. """ - correct_icon = 'check' if valid else 'close' - return self.q(css='.fa-{icon}'.format(icon=correct_icon)).present - - def click_bookmarks_button(self): - """ Click on Bookmarks button """ - self.q(css='.bookmarks-list-button').first.click() - bookmarks_page = BookmarksPage(self.browser, self.course_id) - bookmarks_page.visit() - - def resume_course_from_header(self): - """ - Navigate to courseware using Resume Course button in the header. - """ - self.q(css=self.HEADER_RESUME_COURSE_SELECTOR).first.click() - courseware_page = CoursewarePage(self.browser, self.course_id) - courseware_page.wait_for_page() - - def search_for_term(self, search_term): - """ - Search within a class for a particular term. - """ - self.q(css='.search-form > .search-input').fill(search_term) - self.q(css='.search-form .search-button').click() - return CourseSearchResultsPage(self.browser, self.course_id) - - -class CourseOutlinePage(PageObject): - """ - Course outline fragment of page. - """ - - url = None - - def __init__(self, browser, parent_page): - super(CourseOutlinePage, self).__init__(browser) - self.parent_page = parent_page - self._section_selector = '.outline-item.section' - self._subsection_selector = '.subsection.accordion' - - def is_browser_on_page(self): - return self.parent_page.is_browser_on_page - - @property - def sections(self): - """ - Return a dictionary representation of sections and subsections. - - Example: - - { - 'Introduction': ['Course Overview'], - 'Week 1': ['Lesson 1', 'Lesson 2', 'Homework'] - 'Final Exam': ['Final Exam'] - } - - You can use these titles in `go_to_section` to navigate to the section. - """ - return self._get_outline_structure_as_dictionary() - - @property - def num_sections(self): - """ - Return the number of sections - """ - return len(self._get_sections_as_selenium_webelements()) - - @property - def num_subsections(self, section_title=None): - """ - Return the number of subsections. - - Arguments: - section_title: The section for which to return the number of - subsections. If None, default to the first section. - """ - if section_title: - section_index = self._section_title_to_index(section_title) - if not section_index: - return - else: - section_index = 0 - - sections = self._get_sections_as_selenium_webelements() - subsections = self._get_subsections(sections[section_index]) - return len(subsections) - - @property - def num_units(self): - """ - Return the number of units in the first subsection. - - This method returns the number of units in the horizontal navigation - bar; not the course outline. - """ - return len(self.q(css='.sequence-list-wrapper ol li')) - - def go_to_section(self, section_title, subsection_title): - """ - Go to the section/subsection in the courseware. - Every section must have at least one subsection, so specify - both the section and subsection title. - - Example: - go_to_section("Week 1", "Lesson 1") - """ - subsection_webelements = self._get_subsections_as_selenium_webelements() - subsection_titles = [self._get_outline_element_title(sub_webel) - for sub_webel in subsection_webelements] - - try: - subsection_index = subsection_titles.index(text_type(subsection_title)) - except ValueError: - raise ValueError(u"Could not find subsection '{0}' in section '{1}'".format( - subsection_title, section_title - )) - - target_subsection = subsection_webelements[subsection_index] - units = self._get_units(target_subsection) - - # Click the subsection's first problem and ensure that the page finishes - # reloading - units[0].location_once_scrolled_into_view # pylint: disable=W0104 - units[0].click() - - self._wait_for_course_section(section_title, subsection_title) - - def go_to_section_by_index(self, section_index, subsection_index): - """ - Go to the section/subsection in the courseware. - Every section must have at least one subsection, so specify both the - section and subsection indices. - - Arguments: - section_index: A 0-based index of the section to navigate to. - subsection_index: A 0-based index of the subsection to navigate to. - - """ - try: - section_title = self._section_titles()[section_index] - except IndexError: - raise ValueError(u"Section index '{0}' is out of range.".format(section_index)) - try: - subsection_title = self._subsection_titles(section_index)[subsection_index] - except IndexError: - raise ValueError(u"Subsection index '{0}' in section index '{1}' is out of range.".format( - subsection_index, section_index - )) - - self.go_to_section(section_title, subsection_title) - - def _section_title_to_index(self, section_title): - """ - Get the section title index given the section title. - """ - try: - section_index = self._section_titles().index(section_title) - except ValueError: - raise ValueError(u"Could not find section '{0}'".format(section_title)) - - return section_index - - def resume_course_from_outline(self): - """ - Navigate to courseware using Resume Course button in the header. - """ - self.q(css='.btn.btn-primary.action-resume-course').results[0].click() - courseware_page = CoursewarePage(self.browser, self.parent_page.course_id) - courseware_page.wait_for_page() - - def _section_titles(self): - """ - Return a list of all section titles on the page. - """ - outline_sections = self._get_sections_as_selenium_webelements() - section_titles = [self._get_outline_element_title(section) for section in outline_sections] - return section_titles - - def _subsection_titles(self, section_index): - """ - Return a list of all subsection titles on the page - for the section at index `section_index` (starts at 0). - """ - outline_sections = self._get_sections_as_selenium_webelements() - target_section = outline_sections[section_index] - target_subsections = self._get_subsections(target_section) - subsection_titles = [self._get_outline_element_title(subsection) - for subsection in target_subsections] - return subsection_titles - - def _wait_for_course_section(self, section_title, subsection_title): - """ - Ensures the user navigates to the course content page with the correct section and - subsection. - """ - courseware_page = CoursewarePage(self.browser, self.parent_page.course_id) - courseware_page.wait_for_page() - - # TODO: TNL-6546: Remove this if/visit_course_outline_page - if self.parent_page.course_outline_page: - courseware_page.nav.visit_course_outline_page() - - self.wait_for( - promise_check_func=lambda: courseware_page.nav.is_on_section( - section_title, subsection_title), - description=u"Waiting for course page with section '{0}' and subsection '{1}'".format( - section_title, subsection_title) - ) - - def _get_outline_structure_as_dictionary(self): - ''' - Implements self.sections(). - ''' - outline_dict = OrderedDict() - - try: - outline_sections = self._get_sections_as_selenium_webelements() - except BrokenPromise: - outline_sections = [] - - for section in outline_sections: - subsections = self._get_subsections(section) - section_title = self._get_outline_element_title(section) - subsection_titles = [self._get_outline_element_title(subsection) - for subsection in subsections] - outline_dict[section_title] = subsection_titles - - return outline_dict - - @staticmethod - def _is_html_element_aria_expanded(html_element): - return html_element.get_attribute('aria-expanded') == u'true' - - @staticmethod - def _get_outline_element_title(outline_element): - return outline_element.text.split('\n')[0] - - def _get_subsections(self, section): - self._expand_all_outline_folds() - return section.find_elements_by_css_selector(self._subsection_selector) - - def _get_units(self, subsection): - self._expand_all_outline_folds() - return subsection.find_elements_by_tag_name('a') - - def _get_sections_as_selenium_webelements(self): - self._expand_all_outline_folds() - return self.q(css=self._section_selector).results - - def _get_subsections_as_selenium_webelements(self): - self._expand_all_outline_folds() - return self.q(css=self._subsection_selector).results - - def get_subsection_due_date(self, index=0): - """ - Get the due date for the given index sub-section on the LMS outline. - """ - results = self.q(css='div.details > span.subtitle > span.subtitle-name').results - return results[index].text if results else None - - def _expand_all_outline_folds(self): - ''' - Expands all parts of the collapsible outline. - ''' - expand_button_search_results = self.q( - css='#expand-collapse-outline-all-button' - ).results - - if not expand_button_search_results: - return - - expand_button = expand_button_search_results[0] - - if not self._is_html_element_aria_expanded(expand_button): - expand_button.click() - - -class CourseSearchResultsPage(CoursePage): - """ - Course search page - """ - - # url = "courses/{course_id}/search/?query={query_string}" - - def is_browser_on_page(self): - return self.q(css='.page-content > .search-results').present - - def __init__(self, browser, course_id): - super(CourseSearchResultsPage, self).__init__(browser, course_id) - self.course_id = course_id - - @property - def search_results(self): - return self.q(css='.search-results-item') diff --git a/common/test/acceptance/pages/lms/course_info.py b/common/test/acceptance/pages/lms/course_info.py deleted file mode 100644 index fa56ef6c3c..0000000000 --- a/common/test/acceptance/pages/lms/course_info.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Course info page. -""" - - -from common.test.acceptance.pages.lms.course_page import CoursePage - - -class CourseInfoPage(CoursePage): - """ - Course info. - """ - - url_path = "info" - - def is_browser_on_page(self): - return self.q(css='section.updates').present - - @property - def num_updates(self): - """ - Return the number of updates on the page. - """ - return len(self.q(css='.updates .updates-article').results) - - @property - def handout_links(self): - """ - Return a list of handout assets links. - """ - return self.q(css='section.handouts ol li a').map(lambda el: el.get_attribute('href')).results diff --git a/common/test/acceptance/pages/lms/courseware.py b/common/test/acceptance/pages/lms/courseware.py index a7f848621f..2b06f499d0 100644 --- a/common/test/acceptance/pages/lms/courseware.py +++ b/common/test/acceptance/pages/lms/courseware.py @@ -3,20 +3,12 @@ Courseware page. """ -import re - -from bok_choy.page_object import PageObject, unguarded from bok_choy.promise import EmptyPromise -from selenium.webdriver.common.action_chains import ActionChains -from six.moves import range -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.bookmarks import BookmarksPage -from common.test.acceptance.pages.lms.completion import CompletionOnViewMixin from common.test.acceptance.pages.lms.course_page import CoursePage -class CoursewarePage(CoursePage, CompletionOnViewMixin): +class CoursewarePage(CoursePage): """ Course info. """ @@ -30,102 +22,11 @@ class CoursewarePage(CoursePage, CompletionOnViewMixin): def __init__(self, browser, course_id): super(CoursewarePage, self).__init__(browser, course_id) - self.nav = CourseNavPage(browser, self) + # self.nav = CourseNavPage(browser, self) def is_browser_on_page(self): return self.q(css='.course-content').present - # TODO: TNL-6546: Remove and find callers - @property - def chapter_count_in_navigation(self): - """ - Returns count of chapters available on LHS navigation. - """ - return len(self.q(css='nav.course-navigation a.chapter')) - - # TODO: TNL-6546: Remove and find callers. - @property - def num_sections(self): - """ - Return the number of sections in the sidebar on the page - """ - return len(self.q(css=self.section_selector)) - - # TODO: TNL-6546: Remove and find callers. - @property - def num_subsections(self): - """ - Return the number of subsections in the sidebar on the page, including in collapsed sections - """ - return len(self.q(css=self.subsection_selector)) - - @property - def xblock_components(self): - """ - Return the xblock components within the unit on the page. - """ - return self.q(css=self.xblock_component_selector) - - @property - def num_xblock_components(self): - """ - Return the number of rendered xblocks within the unit on the page - """ - return len(self.xblock_components) - - def xblock_component_type(self, index=0): - """ - Extract rendered xblock component type. - - Returns: - str: xblock module type - index: which xblock to query, where the index is the vertical display within the page - (default is 0) - """ - return self.q(css=self.xblock_component_selector).attrs('data-block-type')[index] - - def xblock_component_html_content(self, index=0): - """ - Extract rendered xblock component html content. - - Returns: - str: xblock module html content - index: which xblock to query, where the index is the vertical display within the page - (default is 0) - - """ - # When Student Notes feature is enabled, it looks for the content inside - # `.edx-notes-wrapper-content` element (Otherwise, you will get an - # additional html related to Student Notes). - element = self.q(css=u'{} .edx-notes-wrapper-content'.format(self.xblock_component_selector)) - if element.first: - return element.attrs('innerHTML')[index].strip() - else: - return self.q(css=self.xblock_component_selector).attrs('innerHTML')[index].strip() - - def verify_tooltips_displayed(self): - """ - Verify that all sequence navigation bar tooltips are being displayed upon mouse hover. - - If a tooltip does not appear, raise a BrokenPromise. - """ - for index, tab in enumerate(self.q(css='#sequence-list > li')): - ActionChains(self.browser).move_to_element(tab).perform() - self.wait_for_element_visibility( - u'#tab_{index} > .sequence-tooltip'.format(index=index), - u'Tab {index} should appear'.format(index=index) - ) - - @property - def course_license(self): - """ - Returns the course license text, if present. Else returns None. - """ - element = self.q(css="#content .container-footer .course-license") - if element.is_present(): - return element.text[0] - return None - def go_to_sequential_position(self, sequential_position): """ Within a section/subsection navigate to the sequential position specified by `sequential_position`. @@ -148,38 +49,13 @@ class CoursewarePage(CoursePage, CompletionOnViewMixin): self.q(css=sequential_position_css).first.click() EmptyPromise(is_at_new_position, "Position navigation fulfilled").fulfill() - @property - def sequential_position(self): - """ - Returns the position of the active tab in the sequence. - """ - tab_id = self._active_sequence_tab.attrs('id')[0] - return int(tab_id.split('_')[1]) - @property def _active_sequence_tab(self): return self.q(css='#sequence-list .nav-item.active') - @property - def is_next_button_enabled(self): - return not self.q(css='.sequence-nav > .sequence-nav-button.button-next.disabled').is_present() - - @property - def is_previous_button_enabled(self): - return not self.q(css='.sequence-nav > .sequence-nav-button.button-previous.disabled').is_present() - def click_next_button_on_top(self): self._click_navigation_button('sequence-nav', 'button-next') - def click_next_button_on_bottom(self): - self._click_navigation_button('sequence-bottom', 'button-next') - - def click_previous_button_on_top(self): - self._click_navigation_button('sequence-nav', 'button-previous') - - def click_previous_button_on_bottom(self): - self._click_navigation_button('sequence-bottom', 'button-previous') - def _click_navigation_button(self, top_or_bottom_class, next_or_previous_class): """ Clicks the navigation button, given the respective CSS classes. @@ -201,542 +77,3 @@ class CoursewarePage(CoursePage, CompletionOnViewMixin): css=u'.{} > .sequence-nav-button.{}'.format(top_or_bottom_class, next_or_previous_class) ).first.click() EmptyPromise(is_at_new_tab_id, "Button navigation fulfilled").fulfill() - - @property - def can_start_proctored_exam(self): - """ - Returns True if the timed/proctored exam timer bar is visible on the courseware. - """ - return self.q(css='button.start-timed-exam[data-start-immediately="false"]').is_present() - - def start_timed_exam(self): - """ - clicks the start this timed exam link - """ - self.q(css=".xblock-student_view .timed-exam .start-timed-exam").first.click() - self.wait_for_element_presence(".proctored_exam_status .exam-timer", "Timer bar") - - def stop_timed_exam(self): - """ - clicks the stop this timed exam link - """ - self.q(css=".proctored_exam_status button.exam-button-turn-in-exam").first.click() - self.wait_for_element_absence(".proctored_exam_status .exam-button-turn-in-exam", "End Exam Button gone") - self.wait_for_element_presence("button[name='submit-proctored-exam']", "Submit Exam Button") - self.q(css="button[name='submit-proctored-exam']").first.click() - self.wait_for_element_absence(".proctored_exam_status .exam-timer", "Timer bar") - - def start_proctored_exam(self): - """ - clicks the start this timed exam link - """ - self.q(css='button.start-timed-exam[data-start-immediately="false"]').first.click() - - # Wait for the unique exam code to appear. - # self.wait_for_element_presence(".proctored-exam-code", "unique exam code") - - def has_submitted_exam_message(self): - """ - Returns whether the "you have submitted your exam" message is present. - This being true implies "the exam contents and results are hidden". - """ - return self.q(css="div.proctored-exam.completed").visible - - def content_hidden_past_due_date(self): - """ - Returns whether the "the due date for this ___ has passed" message is present. - ___ is the type of the hidden content, and defaults to subsection. - This being true implies "the ___ contents are hidden because their due date has passed". - """ - message = "this assignment is no longer available" - if self.q(css="div.seq_content").is_present(): - return False - for html in self.q(css="div.hidden-content").html: - if message in html: - return True - return False - - @property - def entrance_exam_message_selector(self): - """ - Return the entrance exam status message selector on the top of courseware page. - """ - return self.q(css='#content .container section.course-content .sequential-status-message') - - def has_entrance_exam_message(self): - """ - Returns boolean indicating presence entrance exam status message container div. - """ - return self.entrance_exam_message_selector.is_present() - - def has_passed_message(self): - """ - Returns boolean indicating presence of passed message. - """ - return self.entrance_exam_message_selector.is_present() \ - and "You have passed the entrance exam" in self.entrance_exam_message_selector.text[0] - - def has_banner(self): - """ - Returns boolean indicating presence of banner - """ - return self.q(css='.pattern-library-shim').is_present() - - @property - def is_timer_bar_present(self): - """ - Returns True if the timed/proctored exam timer bar is visible on the courseware. - """ - return self.q(css=".proctored_exam_status .exam-timer").is_present() - - def active_usage_id(self): - """ Returns the usage id of active sequence item """ - get_active = lambda el: 'active' in el.get_attribute('class') - attribute_value = lambda el: el.get_attribute('data-id') - return self.q(css='#sequence-list .nav-item').filter(get_active).map(attribute_value).results[0] - - def unit_title_visible(self): - """ Check if unit title is visible """ - return self.q(css='.unit-title').visible - - def bookmark_button_visible(self): - """ Check if bookmark button is visible """ - EmptyPromise(lambda: self.q(css='.bookmark-button').visible, "Bookmark button visible").fulfill() - return True - - @property - def bookmark_button_state(self): - """ Return `bookmarked` if button is in bookmarked state else '' """ - return 'bookmarked' if self.q(css='.bookmark-button.bookmarked').present else '' - - @property - def bookmark_icon_visible(self): - """ Check if bookmark icon is visible on active sequence nav item """ - return self.q(css='.active .bookmark-icon').visible - - def click_bookmark_unit_button(self): - """ Bookmark a unit by clicking on Bookmark button """ - previous_state = self.bookmark_button_state - self.q(css='.bookmark-button').first.click() - EmptyPromise(lambda: self.bookmark_button_state != previous_state, "Bookmark button toggled").fulfill() - - # TODO: TNL-6546: Remove this helper function - def click_bookmarks_button(self): - """ Click on Bookmarks button """ - self.q(css='.bookmarks-list-button').first.click() - bookmarks_page = BookmarksPage(self.browser, self.course_id) - bookmarks_page.visit() - - def is_gating_banner_visible(self): - """ - Check if the gated banner for locked content is visible. - """ - return self.q(css='.problem-header').is_present() \ - and self.q(css='.btn-brand').text[0] == u'Go To Prerequisite Section' \ - and self.q(css='.problem-header').text[0] == u'Content Locked' - - @property - def is_word_cloud_rendered(self): - """ - Check for word cloud fields presence - """ - return self.q(css='.input-cloud').visible - - def input_word_cloud(self, answer_word): - """ - Fill the word cloud fields - - Args: - answer_word(str): An answer words to be filled in the field - """ - self.wait_for_element_visibility('.input-cloud', "Word cloud fields are visible") - css = u'.input_cloud_section label:nth-child({}) .input-cloud' - for index in range(1, len(self.q(css='.input-cloud')) + 1): - self.q(css=css.format(index)).fill(answer_word + str(index)) - - def save_word_cloud(self): - """ - Click save button - """ - self.q(css='.input_cloud_section .action button.save').click() - self.wait_for_ajax() - - @property - def word_cloud_answer_list(self): - """ - Get saved words - - Returns: - list: Return empty when no answer words are present - list: Return populated when answer words are present - """ - - self.wait_for_element_presence('.your_words', "Answer list is present") - if self.q(css='.your_words strong').present: - return self.q(css='.your_words strong').text - else: - return self.q(css='.your_words').text[0] - - def is_error_message_present(self): - """ Check if LTI error is shown """ - return self.q(css=".error_message").is_present() - - def is_iframe_present(self): - """ Check if LTI iframe is present""" - return self.q(css=".ltiLaunchFrame").is_present() - - def is_launch_url_present(self): - """ Check if LTI launch link is present""" - return self.q(css=".link_lti_new_window").is_present() - - def go_to_lti_container(self): - """ Switch to LTI container""" - self.scroll_to_element('.problem-header') - iframe_name = self.q(css='.ltiLaunchFrame').attrs("name")[0] - self.browser.switch_to.frame(iframe_name) - - @property - def get_role_selector(self): - """ - Find and return role selector - """ - self.wait_for_element_visibility( - '.action-preview-select', - 'Role selector element is available' - ) - return self.q(css='.action-preview-select') - - def get_elem_text(self, elem_css): - """ - Find the element with CSS selector given in the argument and return its text - """ - self.wait_for_element_visibility( - elem_css, - 'problem score element is visible' - ) - return self.q(css=elem_css).text[0] - - def is_lti_component_present(self, elem_css): - """ Check if LTI component element with given CSS selector is present""" - return self.q(css=elem_css).is_present() - - -class LTIContentIframe(CoursePage): - """ - LTIContentIframe info - """ - - def is_browser_on_page(self): - return self.q(css=".result").present - - def submit_lti_answer(self, button_css): - """ Click on Submit button""" - self.wait_for_element_presence(button_css, "Button element not present.") - self.q(css=button_css).first.click() - - @property - def lti_content(self): - """ return LTI content""" - self.wait_for_element_presence(".result", "Result element not present.") - content = self.q(css=".result").text[0] - return content - - @property - def get_user_role(self): - """ - return logged in user role text from lti iframe contents(i.e staff or learner) - """ - return self.q(css="h5").text[0] - - def switch_to_default(self): - """ - Switches to default page - """ - self.browser.switch_to_default_content() - - -class CoursewareSequentialTabPage(CoursePage): - """ - Courseware Sequential page - """ - - def __init__(self, browser, course_id, chapter, subsection, position): - super(CoursewareSequentialTabPage, self).__init__(browser, course_id) - self.url_path = "courseware/{}/{}/{}".format(chapter, subsection, position) - - def is_browser_on_page(self): - return self.q(css='nav.sequence-list-wrapper').present - - def get_selected_tab_content(self): - """ - return the body of the sequential currently selected - """ - return self.q(css='#seq_content .xblock').text[0] - - -class CourseNavPage(PageObject): - """ - Handles navigation on the courseware pages, including sequence navigation and - breadcrumbs. - """ - - url = None - - def __init__(self, browser, parent_page): - super(CourseNavPage, self).__init__(browser) - self.parent_page = parent_page - # TODO: TNL-6546: Remove the following - self.course_outline_page = False - - def is_browser_on_page(self): - return self.parent_page.is_browser_on_page - - @property - def breadcrumb_section_title(self): - """ - Returns the section's title from the breadcrumb, or None if one is not found. - """ - label = self.q(css='.breadcrumbs .nav-item-chapter').text - return label[0].strip() if label else None - - @property - def breadcrumb_subsection_title(self): - """ - Returns the subsection's title from the breadcrumb, or None if one is not found - """ - label = self.q(css='.breadcrumbs .nav-item-section').text - return label[0].strip() if label else None - - @property - def breadcrumb_unit_title(self): - """ - Returns the unit's title from the breadcrumb, or None if one is not found - """ - label = self.q(css='.breadcrumbs .nav-item-sequence').text - return label[0].strip() if label else None - - # TODO: TNL-6546: Remove method, outline no longer on courseware page - @property - def sections(self): - """ - Return a dictionary representation of sections and subsections. - - Example: - - { - 'Introduction': ['Course Overview'], - 'Week 1': ['Lesson 1', 'Lesson 2', 'Homework'] - 'Final Exam': ['Final Exam'] - } - - You can use these titles in `go_to_section` to navigate to the section. - """ - # Dict to store the result - nav_dict = dict() - - section_titles = self._section_titles() - - # Get the section titles for each chapter - for sec_index, sec_title in enumerate(section_titles): - - if len(section_titles) < 1: - self.warning(u"Could not find subsections for '{0}'".format(sec_title)) - else: - # Add one to convert list index (starts at 0) to CSS index (starts at 1) - nav_dict[sec_title] = self._subsection_titles(sec_index + 1) - - return nav_dict - - @property - def sequence_items(self): - """ - Return a list of sequence items on the page. - Sequence items are one level below subsections in the course nav. - - Example return value: - ['Chemical Bonds Video', 'Practice Problems', 'Homework'] - """ - seq_css = 'ol#sequence-list>li>.nav-item>.sequence-tooltip' - return self.q(css=seq_css).map(self._clean_seq_titles).results - - # TODO: TNL-6546: Remove method, outline no longer on courseware page - def go_to_section(self, section_title, subsection_title): - """ - Go to the section in the courseware. - Every section must have at least one subsection, so specify - both the section and subsection title. - - Example: - go_to_section("Week 1", "Lesson 1") - """ - - # For test stability, disable JQuery animations (opening / closing menus) - self.browser.execute_script("jQuery.fx.off = true;") - - # Get the section by index - try: - sec_index = self._section_titles().index(section_title) - except ValueError: - self.warning(u"Could not find section '{0}'".format(section_title)) - return - - # Click the section to ensure it's open (no harm in clicking twice if it's already open) - # Add one to convert from list index to CSS index - section_css = u'.course-navigation .chapter:nth-of-type({0})'.format(sec_index + 1) - self.q(css=section_css).first.click() - - # Get the subsection by index - try: - subsec_index = self._subsection_titles(sec_index + 1).index(subsection_title) - except ValueError: - msg = u"Could not find subsection '{0}' in section '{1}'".format(subsection_title, section_title) - self.warning(msg) - return - - # Convert list indices (start at zero) to CSS indices (start at 1) - subsection_css = ( - u".course-navigation .chapter-content-container:nth-of-type({0}) " - ".menu-item:nth-of-type({1})" - ).format(sec_index + 1, subsec_index + 1) - - # Click the subsection and ensure that the page finishes reloading - self.q(css=subsection_css).first.click() - self._on_section_promise(section_title, subsection_title).fulfill() - - def go_to_vertical(self, vertical_title): - """ - Within a section/subsection, navigate to the vertical with `vertical_title`. - """ - - # Get the index of the item in the sequence - all_items = self.sequence_items - - try: - seq_index = all_items.index(vertical_title) - - except ValueError: - msg = u"Could not find sequential '{0}'. Available sequentials: [{1}]".format( - vertical_title, ", ".join(all_items) - ) - self.warning(msg) - - else: - - # Click on the sequence item at the correct index - # Convert the list index (starts at 0) to a CSS index (starts at 1) - seq_css = "ol#sequence-list>li:nth-of-type({0})>.nav-item".format(seq_index + 1) - self.q(css=seq_css).first.click() - # Click triggers an ajax event - self.wait_for_ajax() - - # TODO: TNL-6546: Remove method, outline no longer on courseware page - def _section_titles(self): - """ - Return a list of all section titles on the page. - """ - chapter_css = '.course-navigation .chapter .group-heading' - return self.q(css=chapter_css).map(lambda el: el.text.strip()).results - - # TODO: TNL-6546: Remove method, outline no longer on courseware page - def _subsection_titles(self, section_index): - """ - Return a list of all subsection titles on the page - for the section at index `section_index` (starts at 1). - """ - # Retrieve the subsection title for the section - # Add one to the list index to get the CSS index, which starts at one - subsection_css = ( - u".course-navigation .chapter-content-container:nth-of-type({0}) " - ".menu-item a p:nth-of-type(1)" - ).format(section_index) - - # If the element is visible, we can get its text directly - # Otherwise, we need to get the HTML - # It *would* make sense to always get the HTML, but unfortunately - # the open tab has some child tags that we don't want. - return self.q( - css=subsection_css - ).map( - lambda el: el.text.strip().split('\n')[0] if el.is_displayed() else el.get_attribute('innerHTML').strip() - ).results - - # TODO: TNL-6546: Remove method, outline no longer on courseware page - def _on_section_promise(self, section_title, subsection_title): - """ - Return a `Promise` that is fulfilled when the user is on - the correct section and subsection. - """ - desc = u"currently at section '{0}' and subsection '{1}'".format(section_title, subsection_title) - return EmptyPromise( - lambda: self.is_on_section(section_title, subsection_title), desc - ) - - def go_to_outline(self): - """ - Navigates using breadcrumb to the course outline on the course home page. - - Returns CourseHomePage page object. - """ - # To avoid circular dependency, importing inside the function - from common.test.acceptance.pages.lms.course_home import CourseHomePage - - course_home_page = CourseHomePage(self.browser, self.parent_page.course_id) - self.q(css='.nav-item-course').click() - course_home_page.wait_for_page() - return course_home_page - - @unguarded - def is_on_section(self, section_title, subsection_title): - """ - Return a boolean indicating whether the user is on the section and subsection - with the specified titles. - """ - return self.breadcrumb_section_title == section_title and self.breadcrumb_subsection_title == subsection_title - - # Regular expression to remove HTML span tags from a string - REMOVE_SPAN_TAG_RE = re.compile(r'(.+) 0: - return text_items[0] - else: - return "" - - @property - def available_courses(self): - """ - Return list of the names of available courses (e.g. "999 edX Demonstration Course") - """ - def _get_course_name(el): - return el.text - - return self.q(css='h3.course-title > a').map(_get_course_name).results - - @property - def banner_text(self): - """ - Return the text of the banner on top of the page, or None if - the banner is not present. - """ - message = self.q(css='div.wrapper-msg') - if message.present: - return message.text[0] - return None - - def get_enrollment_mode(self, course_name): - """Get the enrollment mode for a given course on the dashboard. - - Arguments: - course_name (str): The name of the course whose mode should be retrieved. - - Returns: - String, indicating the enrollment mode for the course corresponding to - the provided course name. - - Raises: - Exception, if no course with the provided name is found on the dashboard. - """ - # Filter elements by course name, only returning the relevant course item - course_listing = self.q(css=".course").filter(lambda el: course_name in el.text).results - - if course_listing: - # There should only be one course listing for the provided course name. - # Since 'ENABLE_VERIFIED_CERTIFICATES' is true in the Bok Choy settings, we - # can expect two classes to be present on
elements, one being 'course' - # and the other being the enrollment mode. - enrollment_mode = course_listing[0].get_attribute('class').split('course ')[1] - else: - raise Exception(u"No course named {} was found on the dashboard".format(course_name)) - - return enrollment_mode - - def view_course(self, course_id): - """ - Go to the course with `course_id` (e.g. edx/Open_DemoX/edx_demo_course) - """ - link_css = self._link_css(course_id) - - if link_css is not None: - self.q(css=link_css).first.click() - else: - msg = u"No links found for course {0}".format(course_id) - self.warning(msg) - - def _link_css(self, course_id): - """ - Return a CSS selector for the link to the course with `course_id`. - """ - # Get the link hrefs for all courses - all_links = self.q(css='a.enter-course').map(lambda el: el.get_attribute('href')).results - - # Search for the first link that matches the course id - link_index = None - for index in range(len(all_links)): - if course_id in all_links[index]: - link_index = index - break - - if link_index is not None: - return "a.enter-course:nth-of-type({0})".format(link_index + 1) - else: - return None - - def view_course_unenroll_dialog_message(self, course_id): - """ - Go to the course unenroll dialog message for `course_id` (e.g. edx/Open_DemoX/edx_demo_course) - """ - div_index = self.get_course_actions_link_css(course_id) - button_link_css = "#actions-dropdown-link-{}".format(div_index) - unenroll_css = "#unenroll-{}".format(div_index) - - if button_link_css is not None: - self.q(css=button_link_css).first.click() - self.wait_for_element_visibility(unenroll_css, 'Unenroll message dialog is visible.') - self.q(css=unenroll_css).first.click() - self.wait_for_ajax() - - return { - 'track-info': self.q(css='#track-info').html, - 'refund-info': self.q(css='#refund-info').html - } - - else: - msg = u"No links found for course {0}".format(course_id) - self.warning(msg) - - def get_course_actions_link_css(self, course_id): - """ - Return a index for unenroll button with `course_id`. - """ - # Get the link hrefs for all courses - all_divs = self.q(css='div.wrapper-action-more').map(lambda el: el.get_attribute('data-course-key')).results - - # Search for the first link that matches the course id - div_index = None - for index in range(len(all_divs)): - if course_id in all_divs[index]: - div_index = index - break - - return div_index - - def pre_requisite_message_displayed(self): - """ - Verify if pre-requisite course messages are being displayed. - """ - return self.q(css='div.prerequisites > .tip').visible - - def get_course_listings(self): - """Retrieve the list of course DOM elements""" - return self.q(css='ul.listing-courses') - - def get_course_social_sharing_widget(self, widget_name): - """ Retrieves the specified social sharing widget by its classification """ - return self.q(css='a.action-{}'.format(widget_name)) - - def get_profile_img(self): - """ Retrieves the user's profile image """ - return self.q(css='img.user-image-frame') - def get_courses(self): """ Get all courses shown in the dashboard """ return self.q(css='ul.listing-courses .course-item') - - def get_course_date(self): - """ - Get course date of the first course from dashboard - """ - return self.q(css='ul.listing-courses .course-item:first-of-type .info-date-block').first.text[0] - - def click_username_dropdown(self): - """ - Click username dropdown. - """ - self.q(css='.toggle-user-dropdown').first.click() - - @property - def username_dropdown_link_text(self): - """ - Return list username dropdown links. - """ - return self.q(css='.dropdown-user-menu a').text - - @property - def tabs_link_text(self): - """ - Return the text of all the tabs on the dashboard. - """ - return self.q(css='.nav-tab a').text - - def click_my_profile_link(self): - """ - Click on `Profile` link. - """ - self.q(css='.nav-tab a').nth(1).click() - - def click_account_settings_link(self): - """ - Click on `Account` link. - """ - self.q(css='.dropdown-user-menu a').nth(1).click() - - @property - def language_selector(self): - """ - return language selector - """ - self.wait_for_element_visibility( - '#settings-language-value', - 'Language selector element is available' - ) - return self.q(css='#settings-language-value') - - def is_course_present(self, course_id): - """ - Checks whether course is present or not. - - Arguments: - course_id(str): The unique course id. - - Returns: - bool: True if the course is present. - """ - course_number = CourseKey.from_string(course_id).course - return self.q( - css='#actions-dropdown-link-0[data-course-number="{}"]'.format( - course_number - ) - ).present diff --git a/common/test/acceptance/pages/lms/discovery.py b/common/test/acceptance/pages/lms/discovery.py deleted file mode 100644 index ad9c1eaeb7..0000000000 --- a/common/test/acceptance/pages/lms/discovery.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Course discovery page. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL - - -class CourseDiscoveryPage(PageObject): - """ - Find courses page (main page of the LMS). - """ - - url = BASE_URL + "/courses" - form = "#discovery-form" - - def is_browser_on_page(self): - """ - Loading indicator must be present, but not visible - """ - loading_css = "#loading-indicator" - courses_css = '.courses-listing' - - return self.q(css=courses_css).visible \ - and self.q(css=loading_css).present \ - and not self.q(css=loading_css).visible - - @property - def result_items(self): - """ - Return search result items. - """ - return self.q(css=".courses-list .courses-listing-item") - - @property - def clear_button(self): - """ - Clear all button. - """ - return self.q(css="#clear-all-filters") - - def search(self, string): - """ - Search and wait for ajax. - """ - self.q(css=self.form + ' input[type="text"]').fill(string) - self.q(css=self.form + ' [type="submit"]').click() - self.wait_for_ajax() - - def clear_search(self): - """ - Clear search results. - """ - self.clear_button.click() - self.wait_for_ajax() - - def click_course(self, course_id): - """ - Click on the course - - Args: - course_id(string): ID of the course which is to be clicked - """ - self.q(css='.courses-listing-item a').filter(lambda el: course_id in el.get_attribute('href')).click() diff --git a/common/test/acceptance/pages/lms/discussion.py b/common/test/acceptance/pages/lms/discussion.py index 1acc19735c..711b8eec5f 100644 --- a/common/test/acceptance/pages/lms/discussion.py +++ b/common/test/acceptance/pages/lms/discussion.py @@ -5,70 +5,16 @@ LMS discussion page from contextlib import contextmanager -from bok_choy.javascript import wait_for_js from bok_choy.page_object import PageObject from bok_choy.promise import EmptyPromise -from common.test.acceptance.pages.common.utils import hover from common.test.acceptance.pages.lms.course_page import CoursePage -from common.test.acceptance.tests.helpers import is_focused_on_element -class DiscussionPageMixin(object): - - def is_ajax_finished(self): - return self.browser.execute_script("return jQuery.active") == 0 - - def find_visible_element(self, selector): - """ - Finds a single visible element with the specified selector. - """ - full_selector = selector - if self.root_selector: - full_selector = self.root_selector + " " + full_selector - elements = self.q(css=full_selector) - return next((element for element in elements if element.is_displayed()), None) - - @property - def new_post_button(self): - """ - Returns the new post button if visible, else it returns None. - """ - return self.find_visible_element(".new-post-btn") - - @property - def new_post_form(self): - """ - Returns the new post form if visible, else it returns None. - """ - return self.find_visible_element(".forum-new-post-form") - - def click_new_post_button(self): - """ - Clicks the 'New Post' button. - """ - self.wait_for( - lambda: self.new_post_button, - description="Waiting for new post button" - ) - self.new_post_button.click() - self.wait_for( - lambda: self.new_post_form, - description="Waiting for new post form" - ) - - def click_cancel_new_post(self): - """ - Clicks the 'Cancel' button from the new post form. - """ - self.click_element(".cancel") - self.wait_for( - lambda: not self.new_post_form, - "Waiting for new post form to close" - ) - - -class DiscussionThreadPage(PageObject, DiscussionPageMixin): +class DiscussionThreadPage(PageObject): + """ + Discussion thread page + """ url = None def __init__(self, browser, thread_selector): @@ -127,353 +73,6 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin): "Secondary action menu closed" ).fulfill() - def get_group_visibility_label(self): - """ - Returns the group visibility label shown for the thread. - """ - return self._get_element_text(".group-visibility-label") - - def get_response_total_text(self): - """Returns the response count text, or None if not present""" - self.wait_for_ajax() - return self._get_element_text(".response-count") - - def get_num_displayed_responses(self): - """Returns the number of responses actually rendered""" - return len(self._find_within(".discussion-response")) - - def get_shown_responses_text(self): - """Returns the shown response count text, or None if not present""" - return self._get_element_text(".response-display-count") - - def get_load_responses_button_text(self): - """Returns the load more responses button text, or None if not present""" - return self._get_element_text(".load-response-button") - - def load_more_responses(self): - """Clicks the load more responses button and waits for responses to load""" - self._find_within(".load-response-button").click() - - EmptyPromise( - self.is_ajax_finished, - "Loading more Responses" - ).fulfill() - - def has_add_response_button(self): - """Returns true if the add response button is visible, false otherwise""" - return self.is_element_visible(".add-response-btn") - - def has_discussion_reply_editor(self): - """ - Returns true if the discussion reply editor is is visible - """ - return self.is_element_visible(".discussion-reply-new") - - def click_add_response_button(self): - """ - Clicks the add response button and ensures that the response text - field receives focus - """ - self._find_within(".add-response-btn").first.click() - EmptyPromise( - lambda: self._find_within(".discussion-reply-new textarea:focus").present, - "Response field received focus" - ).fulfill() - - @wait_for_js - def is_response_editor_visible(self, response_id): - """Returns true if the response editor is present, false otherwise""" - return self.is_element_visible(u".response_{} .edit-post-body".format(response_id)) - - @wait_for_js - def is_discussion_body_visible(self): - return self.is_element_visible(".post-body") - - def verify_mathjax_preview_available(self): - """ Checks that MathJax Preview css class is present """ - self.wait_for( - lambda: len(self.q(css=".MathJax_Preview").text) > 0 and self.q(css=".MathJax_Preview").text[0] == "", - description="MathJax Preview is rendered" - ) - - def verify_mathjax_rendered(self): - """ Checks that MathJax css class is present """ - self.wait_for( - lambda: self.is_element_visible(".MathJax_SVG"), - description="MathJax Preview is rendered" - ) - - def is_response_visible(self, comment_id): - """Returns true if the response is viewable onscreen""" - self.wait_for_ajax() - return self.is_element_visible(u".response_{} .response-body".format(comment_id)) - - def is_response_editable(self, response_id): - """Returns true if the edit response button is present, false otherwise""" - with self.secondary_action_menu_open(u".response_{} .discussion-response".format(response_id)): - return self.is_element_visible(u".response_{} .discussion-response .action-edit".format(response_id)) - - def is_response_deletable(self, response_id): - """ - Returns true if the delete response button is present, false otherwise - """ - with self.secondary_action_menu_open(u".response_{} .discussion-response".format(response_id)): - return self.is_element_visible(u".response_{} .discussion-response .action-delete".format(response_id)) - - def get_response_body(self, response_id): - return self._get_element_text(".response_{} .response-body".format(response_id)) - - def start_response_edit(self, response_id): - """Click the edit button for the response, loading the editing view""" - with self.secondary_action_menu_open(u".response_{} .discussion-response".format(response_id)): - self._find_within(u".response_{} .discussion-response .action-edit".format(response_id)).first.click() - EmptyPromise( - lambda: self.is_response_editor_visible(response_id), - "Response edit started" - ).fulfill() - - def get_link_href(self): - """Extracts href attribute of the referenced link""" - link_href = self._find_within(".post-body p a").attrs('href') - return link_href[0] if link_href else None - - def get_response_vote_count(self, response_id): - vote_count_css = '.response_{} .discussion-response .action-vote'.format(response_id) - vote_count_element = self.browser.find_element_by_css_selector(vote_count_css) - # To get the vote count, one must hover over the element first. - hover(self.browser, vote_count_element) - return self._get_element_text(u".response_{} .discussion-response .action-vote .vote-count".format(response_id)) - - def vote_response(self, response_id): - current_count = self.get_response_vote_count(response_id) - self._find_within(u".response_{} .discussion-response .action-vote".format(response_id)).first.click() - self.wait_for( - lambda: current_count != self.get_response_vote_count(response_id), - description=u"Vote updated for {response_id}".format(response_id=response_id) - ) - - def cannot_vote_response(self, response_id): - """Assert that the voting button is not visible on this response""" - return not self.is_element_visible(u".response_{} .discussion-response .action-vote".format(response_id)) - - def is_response_reported(self, response_id): - return self.is_element_visible(".response_{} .discussion-response .post-label-reported".format(response_id)) - - def report_response(self, response_id): - with self.secondary_action_menu_open(".response_{} .discussion-response".format(response_id)): - self._find_within(u".response_{} .discussion-response .action-report".format(response_id)).first.click() - self.wait_for_ajax() - EmptyPromise( - lambda: self.is_response_reported(response_id), - "Response is reported" - ).fulfill() - - def cannot_report_response(self, response_id): - """Assert that the reporting button is not visible on this response""" - return not self.is_element_visible(u".response_{} .discussion-response .action-report".format(response_id)) - - def is_response_endorsed(self, response_id): - return "endorsed" in self._get_element_text(".response_{} .discussion-response .posted-details".format(response_id)) - - def endorse_response(self, response_id): - self._find_within(".response_{} .discussion-response .action-endorse".format(response_id)).first.click() - self.wait_for_ajax() - EmptyPromise( - lambda: self.is_response_endorsed(response_id), - "Response edit started" - ).fulfill() - - def set_response_editor_value(self, response_id, new_body): - """Replace the contents of the response editor""" - self._find_within(u".response_{} .discussion-response .wmd-input".format(response_id)).fill(new_body) - - def verify_link_editor_error_messages_shown(self): - """ - Confirm that the error messages are displayed in the editor. - """ - def errors_visible(): - """ - Returns True if both errors are visible, False otherwise. - """ - return ( - self.q(css="#new-url-input-field-message.has-error").visible and - self.q(css="#new-url-desc-input-field-message.has-error").visible - ) - - self.wait_for(errors_visible, "Form errors should be visible.") - - def add_content_via_editor_button(self, content_type, response_id, url, description, is_decorative=False): - """Replace the contents of the response editor""" - self._find_within( - "#wmd-{}-button-edit-post-body-{}".format( - content_type, - response_id, - ) - ).click() - self.q(css='#new-url-input').fill(url) - self.q(css='#new-url-desc-input').fill(description) - - if is_decorative: - self.q(css='#img-is-decorative').click() - - self.q(css='input[value="OK"]').click() - - def submit_response_edit(self, response_id, new_response_body): - """Click the submit button on the response editor""" - - def submit_response_check_func(): - """ - Tries to click "Update post" and returns True if the post - was successfully updated, False otherwise. - """ - self._find_within( - u".response_{} .discussion-response .post-update".format( - response_id - ) - ).first.click() - - return ( - not self.is_response_editor_visible(response_id) and - self.is_response_visible(response_id) and - self.get_response_body(response_id) == new_response_body - ) - - self.wait_for(submit_response_check_func, "Comment edit succeeded") - - def is_show_comments_visible(self, response_id): - """Returns true if the "show comments" link is visible for a response""" - return self.is_element_visible(u".response_{} .action-show-comments".format(response_id)) - - def show_comments(self, response_id): - """Click the "show comments" link for a response""" - self._find_within(u".response_{} .action-show-comments".format(response_id)).first.click() - EmptyPromise( - lambda: self.is_element_visible(u".response_{} .comments".format(response_id)), - "Comments shown" - ).fulfill() - - def is_add_comment_visible(self, response_id): - """Returns true if the "add comment" form is visible for a response""" - return self.is_element_visible("#wmd-input-comment-body-{}".format(response_id)) - - def is_comment_visible(self, comment_id): - """Returns true if the comment is viewable onscreen""" - return self.is_element_visible(u"#comment_{} .response-body".format(comment_id)) - - def get_comment_body(self, comment_id): - return self._get_element_text("#comment_{} .response-body".format(comment_id)) - - def is_comment_deletable(self, comment_id): - """Returns true if the delete comment button is present, false otherwise""" - with self.secondary_action_menu_open("#comment_{}".format(comment_id)): - return self.is_element_visible(u"#comment_{} .action-delete".format(comment_id)) - - def delete_comment(self, comment_id): - with self.handle_alert(): - with self.secondary_action_menu_open("#comment_{}".format(comment_id)): - self._find_within(u"#comment_{} .action-delete".format(comment_id)).first.click() - EmptyPromise( - lambda: not self.is_comment_visible(comment_id), - "Deleted comment was removed" - ).fulfill() - - def is_comment_editable(self, comment_id): - """Returns true if the edit comment button is present, false otherwise""" - with self.secondary_action_menu_open("#comment_{}".format(comment_id)): - return self.is_element_visible(u"#comment_{} .action-edit".format(comment_id)) - - def is_comment_editor_visible(self, comment_id): - """Returns true if the comment editor is present, false otherwise""" - return self.is_element_visible(".edit-comment-body[data-id='{}']".format(comment_id)) - - def _get_comment_editor_value(self, comment_id): - return self._find_within("#wmd-input-edit-comment-body-{}".format(comment_id)).text[0] - - def start_comment_edit(self, comment_id): - """Click the edit button for the comment, loading the editing view""" - old_body = self.get_comment_body(comment_id) - with self.secondary_action_menu_open("#comment_{}".format(comment_id)): - self._find_within(u"#comment_{} .action-edit".format(comment_id)).first.click() - EmptyPromise( - lambda: ( - self.is_comment_editor_visible(comment_id) and - not self.is_comment_visible(comment_id) and - self._get_comment_editor_value(comment_id) == old_body - ), - "Comment edit started" - ).fulfill() - - def set_comment_editor_value(self, comment_id, new_body): - """Replace the contents of the comment editor""" - self._find_within(u"#comment_{} .wmd-input".format(comment_id)).fill(new_body) - - def submit_comment_edit(self, comment_id, new_comment_body): - """Click the submit button on the comment editor""" - self._find_within(u"#comment_{} .post-update".format(comment_id)).first.click() - self.wait_for_ajax() - EmptyPromise( - lambda: ( - not self.is_comment_editor_visible(comment_id) and - self.is_comment_visible(comment_id) and - self.get_comment_body(comment_id) == new_comment_body - ), - "Comment edit succeeded" - ).fulfill() - - def cancel_comment_edit(self, comment_id, original_body): - """Click the cancel button on the comment editor""" - self._find_within(u"#comment_{} .post-cancel".format(comment_id)).first.click() - EmptyPromise( - lambda: ( - not self.is_comment_editor_visible(comment_id) and - self.is_comment_visible(comment_id) and - self.get_comment_body(comment_id) == original_body - ), - "Comment edit was canceled" - ).fulfill() - - -class DiscussionSortPreferencePage(CoursePage): - """ - Page that contain the discussion board with sorting options - """ - def __init__(self, browser, course_id): - super(DiscussionSortPreferencePage, self).__init__(browser, course_id) - self.url_path = "discussion/forum" - - def is_browser_on_page(self): - """ - Return true if the browser is on the right page else false. - """ - return self.q(css="body.discussion .forum-nav-sort-control").present - - def show_all_discussions(self): - """ Show the list of all discussions. """ - self.q(css=".all-topics").click() - - def get_selected_sort_preference(self): - """ - Return the text of option that is selected for sorting. - """ - # Using this workaround (execute script) to make this test work with Chrome browser - selected_value = self.browser.execute_script( - 'var selected_value = $(".forum-nav-sort-control").val(); return selected_value') - return selected_value - - def change_sort_preference(self, sort_by): - """ - Change the option of sorting by clicking on new option. - """ - self.q(css=u".forum-nav-sort-control option[value='{0}']".format(sort_by)).click() - # Click initiates an ajax call, waiting for it to complete - self.wait_for_ajax() - - def refresh_page(self): - """ - Reload the page. - """ - self.browser.refresh() - class DiscussionTabSingleThreadPage(CoursePage): def __init__(self, browser, course_id, discussion_id, thread_id): @@ -492,167 +91,15 @@ class DiscussionTabSingleThreadPage(CoursePage): def __getattr__(self, name): return getattr(self.thread_page, name) - def show_all_discussions(self): - """ Show the list of all discussions. """ - self.q(css=".all-topics").click() - def close_open_thread(self): with self.thread_page.secondary_action_menu_open(".thread-main-wrapper"): self._find_within(".thread-main-wrapper .action-close").first.click() - def _thread_is_rendered_successfully(self, thread_id): - return self.q(css=".discussion-article[data-id='{}']".format(thread_id)).visible - def click_and_open_thread(self, thread_id): - """ - Click specific thread on the list. - """ - thread_selector = "li[data-id='{}']".format(thread_id) - self.show_all_discussions() - self.q(css=thread_selector).first.click() - EmptyPromise( - lambda: self._thread_is_rendered_successfully(thread_id), - "Thread has been rendered" - ).fulfill() - - def check_threads_rendered_successfully(self, thread_count): - """ - Count the number of threads available on page. - """ - return len(self.q(css=".forum-nav-thread").results) == thread_count - - -class InlineDiscussionPage(PageObject, DiscussionPageMixin): +class DiscussionTabHomePage(CoursePage): """ - Acceptance tests for inline discussions. + Discussion tab home page """ - url = None - - def __init__(self, browser, discussion_id): - super(InlineDiscussionPage, self).__init__(browser) - self.root_selector = ( - u".discussion-module[data-discussion-id='{discussion_id}'] ".format( - discussion_id=discussion_id - ) - ) - - def _find_within(self, selector): - """ - Returns a query corresponding to the given CSS selector within the scope - of this discussion page - """ - return self.q(css=self.root_selector + " " + selector) - - def is_browser_on_page(self): - self.wait_for_ajax() - return self.q(css=self.root_selector).present - - def is_discussion_expanded(self): - return self._find_within(".discussion").present - - def expand_discussion(self): - """Click the link to expand the discussion""" - self._find_within(".discussion-show").first.click() - EmptyPromise( - self.is_discussion_expanded, - "Discussion expanded" - ).fulfill() - - def get_num_displayed_threads(self): - return len(self._find_within(".forum-nav-thread")) - - def element_exists(self, selector): - return self.q(css=self.root_selector + " " + selector).present - - def click_element(self, selector): - self.wait_for_element_presence( - u"{discussion} {selector}".format(discussion=self.root_selector, selector=selector), - u"{selector} is visible".format(selector=selector) - ) - self._find_within(selector).click() - - def is_new_post_button_visible(self): - """ - Check if new post button present and visible - """ - return self._is_element_visible('.new-post-btn') - - @wait_for_js - def _is_element_visible(self, selector): - query = self._find_within(selector) - return query.present and query.visible - - def show_thread(self, thread_id): - """ - Clicks the link for the specified thread to show the detailed view. - """ - self.wait_for_element_presence('.forum-nav-thread-link', 'Thread list has loaded') - thread_selector = u".forum-nav-thread[data-id='{thread_id}'] .forum-nav-thread-link".format(thread_id=thread_id) - self._find_within(thread_selector).first.click() - self.thread_page = InlineDiscussionThreadPage(self.browser, thread_id) # pylint: disable=attribute-defined-outside-init - self.thread_page.wait_for_page() - - -class InlineDiscussionThreadPage(DiscussionThreadPage): - """ - Page object to manipulate an individual thread view in an inline discussion. - """ - def __init__(self, browser, thread_id): - super(InlineDiscussionThreadPage, self).__init__( - browser, - u".discussion-module .discussion-article[data-id='{thread_id}']".format(thread_id=thread_id) - ) - - def is_thread_anonymous(self): - return not self.q(css=".posted-details > .username").present - - @wait_for_js - def check_if_selector_is_focused(self, selector): - """ - Check if selector is focused - """ - return is_focused_on_element(self.browser, selector) - - -class DiscussionUserProfilePage(CoursePage): - - TEXT_NEXT = u'Next >' - TEXT_PREV = u'< Previous' - PAGING_SELECTOR = ".discussion-pagination[data-page-number]" - - def __init__(self, browser, course_id, user_id, username, page=1): - super(DiscussionUserProfilePage, self).__init__(browser, course_id) - self.url_path = "discussion/forum/dummy/users/{}?page={}".format(user_id, page) - self.username = username - - def is_browser_on_page(self): - return ( - self.q(css='.discussion-user-threads[data-course-id="{}"]'.format(self.course_id)).present - and - self.q(css='.user-name').present - and - self.q(css='.user-name').text[0] == self.username - ) - - @wait_for_js - def is_window_on_top(self): - return self.browser.execute_script("return $('html, body').offset().top") == 0 - - def get_shown_thread_ids(self): - elems = self.q(css="li.forum-nav-thread") - return [elem.get_attribute("data-id") for elem in elems] - - def click_on_sidebar_username(self): - self.wait_for_page() - self.q(css='.user-name').first.click() - - def get_user_roles(self): - """Get user roles""" - return self.q(css='.user-roles').text[0] - - -class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): - ALERT_SELECTOR = ".discussion-body .forum-nav .search-alert" def __init__(self, browser, course_id): @@ -662,81 +109,3 @@ class DiscussionTabHomePage(CoursePage, DiscussionPageMixin): def is_browser_on_page(self): return self.q(css=".discussion-body section.home-header").present - - def perform_search(self, text="dummy"): - self.q(css=".search-input").fill(text + chr(10)) - EmptyPromise( - self.is_ajax_finished, - "waiting for server to return result" - ).fulfill() - - def is_element_visible(self, selector): - """ - Returns true if the element matching the specified selector is visible. - """ - query = self.q(css=selector) - return query.present and query.visible - - def is_checkbox_selected(self, selector): - """ - Returns true or false depending upon the matching checkbox is checked. - """ - return self.q(css=selector).selected - - def refresh_and_wait_for_load(self): - """ - Refresh the page and wait for all resources to load. - """ - self.browser.refresh() - self.wait_for_page() - - def get_search_alert_messages(self): - return self.q(css=self.ALERT_SELECTOR + " .message").text - - def get_search_alert_links(self): - return self.q(css=self.ALERT_SELECTOR + " .link-jump") - - def dismiss_alert_message(self, text): - """ - dismiss any search alert message containing the specified text. - """ - def _match_messages(text): - return self.q(css=".search-alert").filter(lambda elem: text in elem.text) - - for alert_id in _match_messages(text).attrs("id"): - self.q(css=u"{}#{} .dismiss".format(self.ALERT_SELECTOR, alert_id)).click() - EmptyPromise( - lambda: _match_messages(text).results == [], - "waiting for dismissed alerts to disappear" - ).fulfill() - - def click_element(self, selector): - """ - Clicks the element specified by selector - """ - element = self.q(css=selector) - return element.click() - - def set_new_post_editor_value(self, new_body): - """ - Set the Discussions new post editor (wmd) with the content in new_body - """ - self.q(css=".wmd-input").fill(new_body) - - def get_new_post_preview_value(self, selector=".wmd-preview > *"): - """ - Get the rendered preview of the contents of the Discussions new post editor - Waits for content to appear, as the preview is triggered on debounced/delayed onchange - """ - self.scroll_to_element(selector) - self.wait_for_element_visibility(selector, "WMD preview pane has contents", timeout=10) - return self.q(css=".wmd-preview").html[0] - - def get_new_post_preview_text(self, selector=".wmd-preview > div"): - """ - Get the rendered preview of the contents of the Discussions new post editor - Waits for content to appear, as the preview is triggered on debounced/delayed onchange - """ - self.scroll_to_element(selector) - self.wait_for_element_visibility(selector, "WMD preview pane has contents", timeout=10) - return self.q(css=".wmd-preview").text[0] diff --git a/common/test/acceptance/pages/lms/edxnotes.py b/common/test/acceptance/pages/lms/edxnotes.py deleted file mode 100644 index f82f66b9d2..0000000000 --- a/common/test/acceptance/pages/lms/edxnotes.py +++ /dev/null @@ -1,547 +0,0 @@ -""" -LMS edxnotes page -""" - - -from bok_choy.page_object import PageLoadError, PageObject, unguarded -from bok_choy.promise import BrokenPromise, EmptyPromise -from selenium.webdriver.common.action_chains import ActionChains - -from common.test.acceptance.pages.common.paging import PaginatedUIMixin -from common.test.acceptance.pages.lms.course_page import CoursePage -from common.test.acceptance.tests.helpers import disable_animations - - -class NoteChild(PageObject): - url = None - BODY_SELECTOR = None - - def __init__(self, browser, item_id): - super(NoteChild, self).__init__(browser) - self.item_id = item_id - - def is_browser_on_page(self): - return self.q(css="{}#{}".format(self.BODY_SELECTOR, self.item_id)).present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to this particular `NoteChild` context - """ - return u"{}#{} {}".format( - self.BODY_SELECTOR, - self.item_id, - selector, - ) - - def _get_element_text(self, selector): - element = self.q(css=self._bounded_selector(selector)).first - if element: - return element.text[0] - else: - return None - - -class EdxNotesChapterGroup(NoteChild): - """ - Helper class that works with chapter (section) grouping of notes in the Course Structure view on the Note page. - """ - BODY_SELECTOR = ".note-group" - - @property - def title(self): - return self._get_element_text(".course-title") - - @property - def subtitles(self): - return [section.title for section in self.children] - - @property - def children(self): - children = self.q(css=self._bounded_selector('.note-section')) - return [EdxNotesSubsectionGroup(self.browser, child.get_attribute("id")) for child in children] - - -class EdxNotesGroupMixin(object): - """ - Helper mixin that works with note groups (used for subsection and tag groupings). - """ - @property - def title(self): - return self._get_element_text(self.TITLE_SELECTOR) - - @property - def children(self): - children = self.q(css=self._bounded_selector('.note')) - return [EdxNotesPageItem(self.browser, child.get_attribute("id")) for child in children] - - @property - def notes(self): - return [section.text for section in self.children] - - -class EdxNotesSubsectionGroup(NoteChild, EdxNotesGroupMixin): - """ - Helper class that works with subsection grouping of notes in the Course Structure view on the Note page. - """ - BODY_SELECTOR = ".note-section" - TITLE_SELECTOR = ".course-subtitle" - - -class EdxNotesTagsGroup(NoteChild, EdxNotesGroupMixin): - """ - Helper class that works with tags grouping of notes in the Tags view on the Note page. - """ - BODY_SELECTOR = ".note-group" - TITLE_SELECTOR = ".tags-title" - - def scrolled_to_top(self, group_index): - """ - Returns True if the group with supplied group)index is scrolled near the top of the page - (expects 10 px padding). - - The group_index must be supplied because JQuery must be used to get this information, and it - does not have access to the bounded selector. - """ - title_selector = "$('" + self.TITLE_SELECTOR + "')[" + str(group_index) + "]" - top_script = "return " + title_selector + ".getBoundingClientRect().top;" - EmptyPromise( - lambda: 8 < self.browser.execute_script(top_script) < 12, - u"Expected tag title '{}' to scroll to top, but was at location {}".format( - self.title, self.browser.execute_script(top_script) - ) - ).fulfill() - # Now also verify that focus has moved to this title (for screen readers): - active_script = "return " + title_selector + " === document.activeElement;" - return self.browser.execute_script(active_script) - - -class EdxNotesPageItem(NoteChild): - """ - Helper class that works with note items on Note page of the course. - """ - BODY_SELECTOR = ".note" - UNIT_LINK_SELECTOR = "a.reference-unit-link" - TAG_SELECTOR = "span.reference-tags" - - def go_to_unit(self, unit_page=None): - self.q(css=self._bounded_selector(self.UNIT_LINK_SELECTOR)).click() - if unit_page is not None: - unit_page.wait_for_page() - - @property - def unit_name(self): - return self._get_element_text(self.UNIT_LINK_SELECTOR) - - @property - def text(self): - return self._get_element_text(".note-comment-p") - - @property - def quote(self): - return self._get_element_text(".note-excerpt") - - @property - def time_updated(self): - return self._get_element_text(".reference-updated-date") - - @property - def tags(self): - """ The tags associated with this note. """ - tag_links = self.q(css=self._bounded_selector(self.TAG_SELECTOR)) - if len(tag_links) == 0: - return None - return[tag_link.text for tag_link in tag_links] - - def go_to_tag(self, tag_name): - """ Clicks a tag associated with the note to change to the tags view (and scroll to the tag group). """ - self.q(css=self._bounded_selector(self.TAG_SELECTOR)).filter(lambda el: tag_name in el.text).click() - - -class EdxNotesPageView(PageObject): - """ - Base class for EdxNotes views: Recent Activity, Location in Course, Search Results. - """ - url = None - BODY_SELECTOR = ".tab-panel" - TAB_SELECTOR = ".tab" - CHILD_SELECTOR = ".note" - CHILD_CLASS = EdxNotesPageItem - - @unguarded - def visit(self): - """ - Open the page containing this page object in the browser. - - Raises: - PageLoadError: The page did not load successfully. - - Returns: - PageObject - """ - self.q(css=self.TAB_SELECTOR).first.click() - try: - return self.wait_for_page() - except BrokenPromise: - raise PageLoadError(u"Timed out waiting to load page '{!r}'".format(self)) - - def is_browser_on_page(self): - return all([ - self.q(css="{}".format(self.BODY_SELECTOR)).present, - self.q(css="{}.is-active".format(self.TAB_SELECTOR)).present, - not self.q(css=".ui-loading").visible, - ]) - - @property - def is_closable(self): - """ - Indicates if tab is closable or not. - """ - return self.q(css=u"{} .action-close".format(self.TAB_SELECTOR)).present - - def close(self): - """ - Closes the tab. - """ - self.q(css=u"{} .action-close".format(self.TAB_SELECTOR)).first.click() - - @property - def children(self): - """ - Returns all notes on the page. - """ - children = self.q(css=self.CHILD_SELECTOR) - return [self.CHILD_CLASS(self.browser, child.get_attribute("id")) for child in children] - - -class RecentActivityView(EdxNotesPageView): - """ - Helper class for Recent Activity view. - """ - BODY_SELECTOR = "#recent-panel" - TAB_SELECTOR = ".tab#view-recent-activity" - - -class CourseStructureView(EdxNotesPageView): - """ - Helper class for Location in Course view. - """ - BODY_SELECTOR = "#structure-panel" - TAB_SELECTOR = ".tab#view-course-structure" - CHILD_SELECTOR = ".note-group" - CHILD_CLASS = EdxNotesChapterGroup - - -class TagsView(EdxNotesPageView): - """ - Helper class for Tags view. - """ - BODY_SELECTOR = "#tags-panel" - TAB_SELECTOR = ".tab#view-tags" - CHILD_SELECTOR = ".note-group" - CHILD_CLASS = EdxNotesTagsGroup - - -class SearchResultsView(EdxNotesPageView): - """ - Helper class for Search Results view. - """ - BODY_SELECTOR = "#search-results-panel" - TAB_SELECTOR = ".tab#view-search-results" - - -class EdxNotesPage(CoursePage, PaginatedUIMixin): - """ - EdxNotes page. - """ - url_path = "edxnotes/" - MAPPING = { - "recent": RecentActivityView, - "structure": CourseStructureView, - "tags": TagsView, - "search": SearchResultsView, - } - - def __init__(self, *args, **kwargs): - super(EdxNotesPage, self).__init__(*args, **kwargs) - self.current_view = self.MAPPING["recent"](self.browser) - - def is_browser_on_page(self): - return self.q(css=".wrapper-student-notes .note-group").visible - - def switch_to_tab(self, tab_name): - """ - Switches to the appropriate tab `tab_name(str)`. - """ - self.current_view = self.MAPPING[tab_name](self.browser) - self.current_view.visit() - - def close_tab(self): - """ - Closes the current view. - """ - self.current_view.close() - self.current_view = self.MAPPING["recent"](self.browser) - - def search(self, text): - """ - Runs search with `text(str)` query. - """ - self.q(css="#search-notes-form #search-notes-input").first.fill(text) - self.q(css='#search-notes-form .search-notes-submit').first.click() - # Frontend will automatically switch to Search results tab when search - # is running, so the view also needs to be changed. - self.current_view = self.MAPPING["search"](self.browser) - if text.strip(): - self.current_view.wait_for_page() - - @property - def tabs(self): - """ - Returns all tabs on the page. - """ - tabs = self.q(css=".tabs .tab-label") - if tabs: - return [x.replace("Current tab\n", "") for x in tabs.text] - else: - return None - - @property - def is_error_visible(self): - """ - Indicates whether error message is visible or not. - """ - return self.q(css=".inline-error").visible - - @property - def error_text(self): - """ - Returns error message. - """ - element = self.q(css=".inline-error").first - if element and self.is_error_visible: - return element.text[0] - else: - return None - - @property - def notes(self): - """ - Returns all notes on the page. - """ - children = self.q(css='.note') - return [EdxNotesPageItem(self.browser, child.get_attribute("id")) for child in children] - - @property - def chapter_groups(self): - """ - Returns all chapter groups on the page. - """ - children = self.q(css='.note-group') - return [EdxNotesChapterGroup(self.browser, child.get_attribute("id")) for child in children] - - @property - def subsection_groups(self): - """ - Returns all subsection groups on the page. - """ - children = self.q(css='.note-section') - return [EdxNotesSubsectionGroup(self.browser, child.get_attribute("id")) for child in children] - - @property - def tag_groups(self): - """ - Returns all tag groups on the page. - """ - children = self.q(css='.note-group') - return [EdxNotesTagsGroup(self.browser, child.get_attribute("id")) for child in children] - - def count(self): - """ Returns the total number of notes in the list """ - return len(self.q(css='div.wrapper-note-excerpts').results) - - -class EdxNoteHighlight(NoteChild): - """ - Helper class that works with notes. - """ - BODY_SELECTOR = "" - ADDER_SELECTOR = ".annotator-adder" - VIEWER_SELECTOR = ".annotator-viewer" - EDITOR_SELECTOR = ".annotator-editor" - NOTE_SELECTOR = ".annotator-note" - - def __init__(self, browser, element, parent_id): - super(EdxNoteHighlight, self).__init__(browser, parent_id) - self.element = element - self.item_id = parent_id - disable_animations(self) - - @property - def is_visible(self): - """ - Returns True if the note is visible. - """ - viewer_is_visible = self.q(css=self._bounded_selector(self.VIEWER_SELECTOR)).visible - editor_is_visible = self.q(css=self._bounded_selector(self.EDITOR_SELECTOR)).visible - return viewer_is_visible or editor_is_visible - - def wait_for_adder_visibility(self): - """ - Waiting for visibility of note adder button. - """ - self.wait_for_element_visibility( - self._bounded_selector(self.ADDER_SELECTOR), "Adder is visible." - ) - - def wait_for_viewer_visibility(self): - """ - Waiting for visibility of note viewer. - """ - self.wait_for_element_visibility( - self._bounded_selector(self.VIEWER_SELECTOR), "Note Viewer is visible." - ) - - def wait_for_editor_visibility(self): - """ - Waiting for visibility of note editor. - """ - self.wait_for_element_visibility( - self._bounded_selector(self.EDITOR_SELECTOR), "Note Editor is visible." - ) - - def wait_for_notes_invisibility(self, text="Notes are hidden"): - """ - Waiting for invisibility of all notes. - """ - selector = self._bounded_selector(".annotator-outer") - self.wait_for_element_invisibility(selector, text) - - def select_and_click_adder(self): - """ - Creates selection for the element and clicks `add note` button. - """ - ActionChains(self.browser).double_click(self.element).perform() - self.wait_for_adder_visibility() - self.q(css=self._bounded_selector(self.ADDER_SELECTOR)).first.click() - self.wait_for_editor_visibility() - return self - - def click_on_highlight(self): - """ - Clicks on the highlighted text. - """ - ActionChains(self.browser).move_to_element(self.element).click().perform() - return self - - def click_on_viewer(self): - """ - Clicks on the note viewer. - """ - self.q(css=self.NOTE_SELECTOR).first.click() - return self - - def show(self): - """ - Hover over highlighted text -> shows note. - """ - ActionChains(self.browser).move_to_element(self.element).perform() - self.wait_for_viewer_visibility() - return self - - def cancel(self): - """ - Clicks cancel button. - """ - self.q(css=self._bounded_selector(".annotator-close")).first.click() - self.wait_for_notes_invisibility("Note is canceled.") - return self - - def save(self): - """ - Clicks save button. - """ - self.q(css=self._bounded_selector(".annotator-save")).first.click() - self.wait_for_notes_invisibility("Note is saved.") - self.wait_for_ajax() - return self - - def remove(self): - """ - Clicks delete button. - """ - self.q(css=self._bounded_selector(".annotator-delete")).first.click() - self.wait_for_notes_invisibility("Note is removed.") - self.wait_for_ajax() - return self - - def edit(self): - """ - Clicks edit button. - """ - self.q(css=self._bounded_selector(".annotator-edit")).first.click() - self.wait_for_editor_visibility() - return self - - @property - def text(self): - """ - Returns text of the note. - """ - self.show() - element = self.q(css=self._bounded_selector(".annotator-annotation > div.annotator-note")) - if element: - text = element.text[0].strip() - else: - text = None - self.cancel() - return text - - @text.setter - def text(self, value): - """ - Sets text for the note. - """ - self.q(css=self._bounded_selector(".annotator-item textarea")).first.fill(value) - - @property - def tags(self): - """ - Returns the tags associated with the note. - - Tags are returned as a list of strings, with each tag as an individual string. - """ - tag_text = [] - self.show() - tags = self.q(css=self._bounded_selector(".annotator-annotation > div.annotator-tags > span.annotator-tag")) - if tags: - for tag in tags: - tag_text.append(tag.text) - self.cancel() - return tag_text - - @tags.setter - def tags(self, tags): - """ - Sets tags for the note. Tags should be supplied as a list of strings, with each tag as an individual string. - """ - self.q(css=self._bounded_selector(".annotator-item input")).first.fill(" ".join(tags)) - - def has_sr_label(self, sr_index, field_index, expected_text): - """ - Returns true iff a screen reader label (of index sr_index) exists for the annotator field with - the specified field_index and text. - """ - label_exists = False - EmptyPromise( - lambda: len(self.q(css=self._bounded_selector("li.annotator-item > label.sr"))) > sr_index, - u"Expected more than '{}' sr labels".format(sr_index) - ).fulfill() - annotator_field_label = self.q(css=self._bounded_selector("li.annotator-item > label.sr"))[sr_index] - for_attrib_correct = annotator_field_label.get_attribute("for") == "annotator-field-" + str(field_index) - if for_attrib_correct and (annotator_field_label.text == expected_text): - label_exists = True - - self.q(css="body").first.click() - self.wait_for_notes_invisibility() - - return label_exists diff --git a/common/test/acceptance/pages/lms/index.py b/common/test/acceptance/pages/lms/index.py deleted file mode 100644 index ce4bfa6961..0000000000 --- a/common/test/acceptance/pages/lms/index.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -""" -LMS index (home) page. -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL - -BANNER_SELECTOR = 'section.home header div.outer-wrapper div.title .heading-group h1' -INTRO_VIDEO_SELECTOR = 'div.play-intro' -VIDEO_MODAL_SELECTOR = 'section#video-modal.modal.home-page-video-modal.video-modal' - - -class IndexPage(PageObject): - """ - LMS index (home) page, the default landing page for Open edX users when they are not logged in - """ - url = "{base}/".format(base=BASE_URL) - - def is_browser_on_page(self): - """ - Returns a browser query object representing the video modal element - """ - element = self.q(css=BANNER_SELECTOR) - return element.visible and element.text[0].startswith("Welcome to ") - - @property - def banner_element(self): - """ - Returns a browser query object representing the video modal element - """ - return self.q(css=BANNER_SELECTOR) - - @property - def intro_video_element(self): - """ - Returns a browser query object representing the video modal element - """ - return self.q(css=INTRO_VIDEO_SELECTOR) - - @property - def video_modal_element(self): - """ - Returns a browser query object representing the video modal element - """ - return self.q(css=VIDEO_MODAL_SELECTOR) - - @property - def footer_links(self): - """Return a list of the text of the links in the page footer.""" - return self.q(css='.nav-colophon a').attrs('text') diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index fbaf3091b7..8661d0c795 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -5,9 +5,7 @@ Instructor (2) dashboard page. import os -import os.path -import six from bok_choy.page_object import PageObject from bok_choy.promise import EmptyPromise, Promise @@ -50,41 +48,6 @@ class InstructorDashboardPage(CoursePage): cohort_management_section.wait_for_page() return cohort_management_section - def select_discussion_management(self): - """ - Selects the Discussion tab and returns the DiscussionmanagementSection - """ - self.q(css='[data-section="discussions_management"').first.click() - discussion_management_section = DiscussionManagementSection(self.browser) - discussion_management_section.wait_for_page() - return discussion_management_section - - def is_discussion_management_visible(self): - """ - Is the Discussion tab visible - """ - return self.q(css='[data-section="discussions_management"').visible - - def select_data_download(self): - """ - Selects the data download tab and returns a DataDownloadPage. - """ - self.q(css='[data-section="data_download"]').first.click() - data_download_section = DataDownloadPage(self.browser) - data_download_section.wait_for_page() - return data_download_section - - def select_student_admin(self, admin_class): - """ - Selects the student admin tab and returns the requested - admin section. - admin_class should be a subclass of StudentAdminPage. - """ - self.q(css='[data-section="student_admin"]').first.click() - student_admin_section = admin_class(self.browser) - student_admin_section.wait_for_page() - return student_admin_section - def select_certificates(self): """ Selects the certificates tab and returns the CertificatesSection @@ -94,15 +57,6 @@ class InstructorDashboardPage(CoursePage): certificates_section.wait_for_page() return certificates_section - def select_special_exams(self): - """ - Selects the timed exam tab and returns the Special Exams Section - """ - self.q(css='[data-section="special_exams"]').first.click() - timed_exam_section = SpecialExamsPage(self.browser) - timed_exam_section.wait_for_page() - return timed_exam_section - def select_bulk_email(self): """ Selects the email tab and returns the bulk email section @@ -112,22 +66,6 @@ class InstructorDashboardPage(CoursePage): email_section.wait_for_page() return email_section - def select_ecommerce_tab(self): - """ - Selects the E-commerce tab and returns an EcommercePage. - """ - self.q(css='[data-section="e-commerce"]').first.click() - ecommerce_section = EcommercePage(self.browser) - ecommerce_section.wait_for_page() - return ecommerce_section - - def is_rescore_unsupported_message_visible(self): - if (self.q(css='.request-response-error').present): - return u'This component cannot be rescored.' in six.text_type( - self.q(css='.request-response-error').html - ) - return False - @staticmethod def get_asset_path(file_name): """ @@ -151,117 +89,6 @@ class InstructorDashboardPage(CoursePage): return os.sep.join(folders_list_in_path) -class BulkEmailPage(PageObject): - """ - Bulk email section of the instructor dashboard. - This feature is controlled by an admin panel feature flag, which is turned on via database fixture for testing. - """ - url = None - - def is_browser_on_page(self): - return self.q(css='[data-section=send_email].active-section').present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to the bulk-email context. - """ - return u'.send-email {}'.format(selector) - - def _select_recipient(self, recipient): - """ - Selects the specified recipient from the selector. Assumes that recipient is not None. - """ - recipient_selector_css = "input[name='send_to'][value='{}']".format(recipient) - self.q(css=self._bounded_selector(recipient_selector_css))[0].click() - - def send_message(self, recipients): - """ - Send a test message to the specified recipient. - """ - send_css = "input[name='send']" - test_subject = "Hello" - test_body = "This is a test email" - - for recipient in recipients: - self._select_recipient(recipient) - self.q(css=self._bounded_selector("input[name='subject']")).fill(test_subject) - self.q(css=self._bounded_selector("iframe#mce_0_ifr"))[0].click() - self.q(css=self._bounded_selector("iframe#mce_0_ifr"))[0].send_keys(test_body) - - with self.handle_alert(confirm=True): - self.q(css=self._bounded_selector(send_css)).click() - - def verify_message_queued_successfully(self): - """ - Verifies that the "you email was queued" message appears. - - Note that this does NOT ensure the message gets sent successfully, that functionality - is covered by the bulk_email unit tests. - """ - confirmation_selector = self._bounded_selector(".msg-confirm") - expected_text = u"Your email message was successfully queued for sending." - EmptyPromise( - lambda: expected_text in self.q(css=confirmation_selector)[0].text, - "Message Queued Confirmation" - ).fulfill() - - -class MembershipPage(PageObject): - """ - Membership section of the Instructor dashboard. - """ - url = None - - def is_browser_on_page(self): - return self.q(css='[data-section=membership].active-section').present - - def select_auto_enroll_section(self): - """ - Returns the MembershipPageAutoEnrollSection page object. - """ - return MembershipPageAutoEnrollSection(self.browser) - - def batch_beta_tester_addition(self): - """ - Returns the MembershipPageBetaTesterSection page object. - """ - return MembershipPageBetaTesterSection(self.browser) - - -class SpecialExamsPage(PageObject): - """ - Timed exam section of the Instructor dashboard. - """ - url = None - - def is_browser_on_page(self): - return self.q(css='[data-section=special_exams].active-section').present - - def select_allowance_section(self): - """ - Expand the allowance section - """ - allowance_section = SpecialExamsPageAllowanceSection(self.browser) - if not self.q(css="div.wrap #ui-accordion-proctoring-accordion-header-0[aria-selected=true]").present: - self.q(css="div.wrap #ui-accordion-proctoring-accordion-header-0").click() - self.wait_for_element_presence("div.wrap #ui-accordion-proctoring-accordion-header-0[aria-selected=true]", - "Allowance Section") - allowance_section.wait_for_page() - return allowance_section - - def select_exam_attempts_section(self): - """ - Expand the Student Attempts Section - """ - exam_attempts_section = SpecialExamsPageAttemptsSection(self.browser) - if not self.q(css="div.wrap #ui-accordion-proctoring-accordion-header-1[aria-selected=true]").present: - self.q(css="div.wrap #ui-accordion-proctoring-accordion-header-1").click() - self.wait_for_element_presence("div.wrap #ui-accordion-proctoring-accordion-header-1[aria-selected=true]", - "Attempts Section") - exam_attempts_section.wait_for_page() - return exam_attempts_section - - class CohortManagementSection(PageObject): """ The Cohort Management section of the Instructor dashboard. @@ -690,147 +517,31 @@ class CohortManagementSection(PageObject): self.q(css=self._bounded_selector('.wrapper-cohort-supplemental')).visible) -class DiscussionManagementSection(PageObject): - +class BulkEmailPage(PageObject): + """ + Bulk email section of the instructor dashboard. + This feature is controlled by an admin panel feature flag, which is turned on via database fixture for testing. + """ url = None - discussion_form_selectors = { - 'course-wide': '.cohort-course-wide-discussions-form', - 'inline': '.cohort-inline-discussions-form', - 'scheme': '.division-scheme-container', - } + def is_browser_on_page(self): + return self.q(css='[data-section=send_email].active-section').present - NOT_DIVIDED_SCHEME = "none" - COHORT_SCHEME = "cohort" - ENROLLMENT_TRACK_SCHEME = "enrollment_track" + +class MembershipPage(PageObject): + """ + Membership section of the Instructor dashboard. + """ + url = None def is_browser_on_page(self): - return self.q(css=self.discussion_form_selectors['course-wide']).present + return self.q(css='[data-section=membership].active-section').present - def _bounded_selector(self, selector): + def select_auto_enroll_section(self): """ - Return `selector`, but limited to the divided discussion management context. + Returns the MembershipPageAutoEnrollSection page object. """ - return u'.discussions-management {}'.format(selector) - - def is_save_button_disabled(self, key): - """ - Returns the status for form's save button, enabled or disabled. - """ - save_button_css = u'%s %s' % (self.discussion_form_selectors[key], '.action-save') - disabled = self.q(css=self._bounded_selector(save_button_css)).attrs('disabled') - return disabled[0] == 'true' - - def discussion_topics_visible(self): - """ - Returns the visibility status of divide discussion controls. - """ - return (self.q(css=self._bounded_selector('.course-wide-discussions-nav')).visible and - self.q(css=self._bounded_selector('.inline-discussions-nav')).visible) - - def divided_discussion_heading_is_visible(self, key): - """ - Returns the text of discussion topic headings if it exists, otherwise return False. - """ - form_heading_css = u'%s %s' % (self.discussion_form_selectors[key], '.subsection-title') - discussion_heading = self.q(css=self._bounded_selector(form_heading_css)) - - if len(discussion_heading) == 0: - return False - return discussion_heading.first.text[0] - - def select_always_inline_discussion(self): - """ - Selects the always_divide_inline_discussions radio button. - """ - self.q(css=self._bounded_selector(".check-all-inline-discussions")).first.click() - - def inline_discussion_topics_disabled(self): - """ - Returns the status of inline discussion topics, enabled or disabled. - """ - inline_topics = self.q(css=self._bounded_selector('.check-discussion-subcategory-inline')) - return all(topic.get_attribute('disabled') == 'true' for topic in inline_topics) - - def save_discussion_topics(self, key): - """ - Saves the discussion topics. - """ - save_button_css = u'%s %s' % (self.discussion_form_selectors[key], '.action-save') - self.q(css=self._bounded_selector(save_button_css)).first.click() - - def always_inline_discussion_selected(self): - """ - Returns true if always_divide_inline_discussions radio button is selected. - """ - return len(self.q(css=self._bounded_selector(".check-all-inline-discussions:checked"))) > 0 - - def divide_some_inline_discussion_selected(self): - """ - Returns true if divide_some_inline_discussions radio button is selected. - """ - return len(self.q(css=self._bounded_selector(".check-cohort-inline-discussions:checked"))) > 0 - - def select_divide_some_inline_discussion(self): - """ - Selects the divide_some_inline_discussions radio button. - """ - self.q(css=self._bounded_selector(".check-cohort-inline-discussions")).first.click() - - def get_divided_topics_count(self, key): - """ - Returns the count for divided topics. - """ - divided_topics = self.q(css=self._bounded_selector('.check-discussion-subcategory-%s:checked' % key)) - return len(divided_topics.results) - - def select_discussion_topic(self, key): - """ - Selects discussion topic checkbox by clicking on it. - """ - self.q(css=self._bounded_selector(".check-discussion-subcategory-%s" % key)).first.click() - - def get_divide_discussions_message(self, key, msg_type="confirmation"): - """ - Returns the message related to modifying discussion topics. - """ - title_css = u"%s .message-%s .message-title" % (self.discussion_form_selectors[key], msg_type) - - EmptyPromise( - lambda: self.q(css=self._bounded_selector(title_css)), - "Waiting for message to appear" - ).fulfill() - - message_title = self.q(css=self._bounded_selector(title_css)) - - if len(message_title.results) == 0: - return '' - return message_title.first.text[0] - - def is_category_selected(self): - """ - Returns the status for category checkboxes. - """ - return self.q(css=self._bounded_selector('.check-discussion-category:checked')).is_present() - - def get_selected_scheme(self): - """ - Returns the ID of the selected discussion division scheme - ("NOT_DIVIDED_SCHEME", "COHORT_SCHEME", or "ENROLLMENT_TRACK_SCHEME)". - """ - return self.q(css=self._bounded_selector('.division-scheme:checked')).first.attrs('value')[0] - - def select_division_scheme(self, scheme): - """ - Selects the radio button associated with the specified division scheme. - """ - self.q(css=self._bounded_selector("input.%s" % scheme)).first.click() - - def division_scheme_visible(self, scheme): - """ - Returns whether or not the specified scheme is visible as an option. - """ - return self.q(css=self._bounded_selector("input.%s" % scheme)).visible + return MembershipPageAutoEnrollSection(self.browser) class MembershipPageAutoEnrollSection(PageObject): @@ -940,413 +651,6 @@ class MembershipPageAutoEnrollSection(PageObject): return self.q(css=u"{} h3".format(notification_selector)).text -class MembershipPageBetaTesterSection(PageObject): - """ - Beta tester section of the Membership tab of the Instructor dashboard. - """ - url = None - - batch_beta_tester_heading_selector = '#heading-batch-beta-testers' - batch_beta_tester_selector = '.batch-beta-testers' - - def is_browser_on_page(self): - return self.q(css=self.batch_beta_tester_heading_selector).present - - def fill_batch_beta_tester_addition_text_box(self, username): - """ - Fill in the form with the provided username and submit it. - """ - username_selector = u"{} textarea".format(self.batch_beta_tester_selector) - enrollment_button = u"{} .enrollment-button[data-action='add']".format(self.batch_beta_tester_selector) - - # Fill the username after the username selector is visible. - self.wait_for_element_visibility(username_selector, 'username field is visible') - self.q(css=username_selector).fill(username) - - # Verify enrollment button is present before clicking - self.wait_for_element_visibility(enrollment_button, 'Add beta testers button') - self.q(css=enrollment_button).click() - - def get_notification_text(self): - """ - Check notification div is visible and have message. - """ - notification_selector = u'{} .request-response'.format(self.batch_beta_tester_selector) - self.wait_for_element_visibility(notification_selector, 'Notification div is visible') - notification_header_text = self.q(css=u"{} h3".format(notification_selector)).text - notification_username = self.q(css=u"{} li".format(notification_selector)).text - return notification_header_text, notification_username - - -class SpecialExamsPageAllowanceSection(PageObject): - """ - Allowance section of the Instructor dashboard's Special Exams tab. - """ - url = None - - def is_browser_on_page(self): - return self.q(css="div.wrap #ui-accordion-proctoring-accordion-header-0[aria-selected=true]").present - - @property - def is_add_allowance_button_visible(self): - """ - Returns True if the Add Allowance button is present. - """ - return self.q(css="a#add-allowance").present - - @property - def is_allowance_record_visible(self): - """ - Returns True if the Add Allowance button is present. - """ - return self.q(css="table.allowance-table tr.allowance-items").present - - @property - def is_add_allowance_popup_visible(self): - """ - Returns True if the Add Allowance popup and it's all assets are present. - """ - return self.q(css="div.modal div.modal-header").present and self._are_all_assets_present() - - def _are_all_assets_present(self): - """ - Returns True if all the assets present in add allowance popup/form - """ - return ( - self.q(css="select#proctored_exam").present and - self.q(css="label#exam_type_label").present and - self.q(css="input#allowance_value").present and - self.q(css="input#user_info").present and - self.q(css="input#addNewAllowance").present - ) and ( - # This will be present if exam is proctored - self.q(css="select#allowance_type").present or - # This will be present if exam is timed - self.q(css="label#timed_exam_allowance_type").present - ) - - def click_add_allowance_button(self): - """ - Click the add allowance button - """ - self.q(css="a#add-allowance").click() - self.wait_for_element_presence("div.modal div.modal-header", "Popup should be visible") - - def submit_allowance_form(self, allowed_minutes, username): - """ - Fill and submit the allowance - """ - self.q(css='input#allowance_value').fill(allowed_minutes) - self.q(css='input#user_info').fill(username) - self.q(css="input#addNewAllowance").click() - self.wait_for_element_absence("div.modal div.modal-header", "Popup should be hidden") - self.wait_for_ajax() - - -class SpecialExamsPageAttemptsSection(PageObject): - """ - Exam Attempts section of the Instructor dashboard's Special Exams tab. - """ - url = None - - def is_browser_on_page(self): - return (self.q(css="div.wrap #ui-accordion-proctoring-accordion-header-1[aria-selected=true]").present and - self.q(css="#search_attempt_id").present) - - @property - def is_search_text_field_visible(self): - """ - Returns True if the search field is present - """ - return self.q(css="#search_attempt_id").present - - @property - def is_student_attempt_visible(self): - """ - Returns True if a row with the Student's attempt is present - """ - return self.q(css="a.remove-attempt").present - - def remove_student_attempt(self): - """ - Clicks the "x" to remove the Student's attempt. - """ - with self.handle_alert(confirm=True): - self.q(css=".remove-attempt").first.click() - self.wait_for_element_absence(".remove-attempt", "exam attempt") - - -class DataDownloadPage(PageObject): - """ - Data Download section of the Instructor dashboard. - """ - url = None - - def is_browser_on_page(self): - return self.q(css='[data-section=data_download].active-section').present - - @property - def generate_student_report_button(self): - """ - Returns the "Download profile information as a CSV" button. - """ - return self.q(css='input[name=list-profiles-csv]') - - @property - def enrolled_student_profile_button(self): - """ - Returns the "List enrolled students' profile information" button. - """ - return self.q(css='input[name=list-profiles]') - - @property - def enrolled_student_profile_button_present(self): - """ - Checks for the presence of Enrolled Student Profile Button - """ - return self.q(css='input[name=list-profiles]').present - - @property - def generate_grading_configuration_button(self): - """ - Returns the "Grading Configuration" button. - """ - return self.q(css='input[name="dump-gradeconf"]') - - @property - def generate_grade_report_button(self): - """ - Returns the "Generate Grade Report" button. - """ - return self.q(css='input[name=calculate-grades-csv]') - - @property - def generate_problem_report_button(self): - """ - Returns the "Generate Problem Grade Report" button. - """ - return self.q(css='input[name=problem-grade-report]') - - @property - def report_download_links(self): - """ - Returns the download links for the current page. - """ - return self.q(css="#report-downloads-table .file-download-link>a") - - @property - def generate_ora2_response_report_button(self): - """ - Returns the ORA2 response download button for the current page. - """ - return self.q(css='input[name=export-ora2-data]') - - def wait_for_available_report(self): - """ - Waits for a downloadable report to be available. - """ - EmptyPromise( - lambda: len(self.report_download_links) >= 1, 'Waiting for downloadable report' - ).fulfill() - - def get_available_reports_for_download(self): - """ - Returns a list of all the available reports for download. - """ - return self.report_download_links.map(lambda el: el.text) - - @property - def student_profile_information(self): - """ - Returns Student profile information - """ - return self.q(css='#data-student-profiles-table').text - - @property - def grading_config_text(self): - """ - Returns grading configuration text - """ - self.wait_for_element_visibility('#data-grade-config-text', 'Grading Configurations are visible') - return self.q(css='#data-grade-config-text').text[0] - - -class StudentAdminPage(PageObject): - """ - Student admin section of the Instructor dashboard. - """ - url = None - PAGE_SELECTOR = 'section#student_admin' - CONTAINER = None - - PROBLEM_INPUT_NAME = None - STUDENT_EMAIL_INPUT_NAME = None - - RESET_ATTEMPTS_BUTTON_NAME = None - RESCORE_BUTTON_NAME = None - RESCORE_IF_HIGHER_BUTTON_NAME = None - DELETE_STATE_BUTTON_NAME = None - - BACKGROUND_TASKS_BUTTON_NAME = None - TASK_HISTORY_TABLE_NAME = None - - def is_browser_on_page(self): - """ - Confirms student admin section is present - """ - return self.q(css='[data-section=student_admin].active-section').present - - def _input_with_name(self, input_name): - """ - Returns the input box with the given name - for this object's container. - """ - return self.q(css=u'{} input[name={}]'.format(self.CONTAINER, input_name)) - - @property - def problem_location_input(self): - """ - Returns input box for problem location - """ - return self._input_with_name(self.PROBLEM_INPUT_NAME) - - def set_problem_location(self, problem_location): - """ - Returns input box for problem location - """ - input_box = self.problem_location_input.first.results[0] - input_box.send_keys(six.text_type(problem_location)) - - @property - def student_email_or_username_input(self): - """ - Returns email address/username input box. - """ - return self._input_with_name(self.STUDENT_EMAIL_INPUT_NAME) - - def set_student_email_or_username(self, email_or_username): - """ - Sets given email or username as value of - student email/username input box. - """ - input_box = self.student_email_or_username_input.first.results[0] - input_box.send_keys(email_or_username) - - @property - def reset_attempts_button(self): - """ - Returns reset student attempts button. - """ - return self._input_with_name(self.RESET_ATTEMPTS_BUTTON_NAME) - - @property - def rescore_button(self): - """ - Returns rescore button. - """ - return self._input_with_name(self.RESCORE_BUTTON_NAME) - - @property - def rescore_if_higher_button(self): - """ - Returns rescore if higher button. - """ - return self._input_with_name(self.RESCORE_IF_HIGHER_BUTTON_NAME) - - @property - def delete_state_button(self): - """ - Returns delete state button. - """ - return self._input_with_name(self.DELETE_STATE_BUTTON_NAME) - - @property - def task_history_button(self): - """ - Return Background Tasks History button. - """ - return self._input_with_name(self.BACKGROUND_TASKS_BUTTON_NAME) - - @property - def running_tasks_section(self): - """ - Returns the "Pending Instructor Tasks" section. - """ - return self.get_selector('div.running-tasks-container') - - def get_selector(self, css_selector): - """ - Makes query selector by pre-pending student admin section - """ - return self.q(css=' '.join([self.PAGE_SELECTOR, css_selector])) - - def wait_for_task_history_table(self): - """ - Waits until the task history table is visible. - """ - def check_func(): - """ - Promise Check Function - """ - query = self.q(css=u"{} .{}".format(self.CONTAINER, self.TASK_HISTORY_TABLE_NAME)) - return query.visible, query - - return Promise(check_func, "Waiting for student admin task history table to be visible.").fulfill() - - def wait_for_task_completion(self, expected_task_string): - """ - Waits until the task history table is visible. - """ - def check_func(): - """ - Promise Check Function - """ - self.task_history_button.click() - table = self.wait_for_task_history_table() - return len(table) > 0 and expected_task_string in table.results[0].text - - return EmptyPromise(check_func, "Waiting for student admin task to complete.").fulfill() - - def click_grade_book_link(self): - self.wait_for_element_presence('.gradebook-link', "Grade book link is not present.") - self.q(css='.gradebook-link').first.click() - - -class GradeBookPage(StudentAdminPage): - """ - Grade Book section of the Instructor dashboard. - """ - url = None - - def is_browser_on_page(self): - """ - Confirms grade book section is present - """ - return self.q(css='.grade-table').present - - def get_value_in_the_grade_book(self, title, index): - """Find the element with given CSS selector, index and return its text""" - return self.q(css='.grade-table td[title^="{}"]'.format(title)).text[index] - - -class StudentSpecificAdmin(StudentAdminPage): - """ - Student specific section of the Student Admin page. - """ - CONTAINER = ".student-grade-container" - - PROBLEM_INPUT_NAME = "problem-select-single" - STUDENT_EMAIL_INPUT_NAME = "student-select-grade" - - RESET_ATTEMPTS_BUTTON_NAME = "reset-attempts-single" - RESCORE_BUTTON_NAME = "rescore-problem-single" - RESCORE_IF_HIGHER_BUTTON_NAME = "rescore-problem-if-higher-single" - DELETE_STATE_BUTTON_NAME = "delete-state-single" - - BACKGROUND_TASKS_BUTTON_NAME = "task-history-single" - TASK_HISTORY_TABLE_NAME = "task-history-single-table" - - class CertificatesPage(PageObject): """ Certificates section of the Instructor dashboard. @@ -1354,195 +658,5 @@ class CertificatesPage(PageObject): url = None PAGE_SELECTOR = 'section#certificates' - def wait_for_certificate_exceptions_section(self): - """ - Wait for Certificate Exceptions to be rendered on page - """ - self.wait_for_element_visibility( - 'div.certificate-exception-container', - 'Certificate Exception Section is visible' - ) - self.wait_for_element_visibility('#add-exception', 'Add Exception button is visible') - - def wait_for_certificate_invalidations_section(self): # pylint: disable=invalid-name - """ - Wait for certificate invalidations section to be rendered on page - """ - self.wait_for_element_visibility( - 'div.certificate-invalidation-container', - 'Certificate invalidations section is visible.' - ) - self.wait_for_element_visibility('#invalidate-certificate', 'Invalidate Certificate button is visible') - - def refresh(self): - """ - Refresh Certificates Page and wait for the page to load completely. - """ - self.browser.refresh() - self.wait_for_page() - def is_browser_on_page(self): return self.q(css='[data-section=certificates].active-section').present - - def get_selector(self, css_selector): - """ - Makes query selector by pre-pending certificates section - """ - return self.q(css=' '.join([self.PAGE_SELECTOR, css_selector])) - - def add_certificate_exception(self, student, free_text_note): - """ - Add Certificate Exception for 'student'. - """ - self.wait_for_element_visibility('#add-exception', 'Add Exception button is visible') - - self.get_selector('#certificate-exception').fill(student) - self.get_selector('#notes').fill(free_text_note) - self.get_selector('#add-exception').click() - - self.wait_for_ajax() - self.wait_for( - lambda: student in self.get_selector('div.white-listed-students table tr:last-child td').text, - description='Certificate Exception added to list' - ) - - def remove_first_certificate_exception(self): - """ - Remove Certificate Exception from the white list. - """ - self.wait_for_element_visibility('#add-exception', 'Add Exception button is visible') - self.get_selector('div.white-listed-students table tr td .delete-exception').first.click() - self.wait_for_ajax() - - def click_generate_certificate_exceptions_button(self): # pylint: disable=invalid-name - """ - Click 'Generate Exception Certificates' button in 'Certificates Exceptions' section - """ - self.get_selector('#generate-exception-certificates').click() - - def fill_user_name_field(self, student): - """ - Fill username/email field with given text - """ - self.get_selector('#certificate-exception').fill(student) - - def click_add_exception_button(self): - """ - Click 'Add Exception' button in 'Certificates Exceptions' section - """ - self.get_selector('#add-exception').click() - - def add_certificate_invalidation(self, student, notes): - """ - Add certificate invalidation for 'student'. - """ - self.wait_for_element_visibility('#invalidate-certificate', 'Invalidate Certificate button is visible') - - self.get_selector('#certificate-invalidation-user').fill(student) - self.get_selector('#certificate-invalidation-notes').fill(notes) - self.get_selector('#invalidate-certificate').click() - - self.wait_for_ajax() - self.wait_for( - lambda: student in self.get_selector('div.invalidation-history table tr:last-child td').text, - description='Certificate invalidation added to list.' - ) - - def remove_first_certificate_invalidation(self): - """ - Remove certificate invalidation from the invalidation list. - """ - self.wait_for_element_visibility('#invalidate-certificate', 'Invalidate Certificate button is visible') - self.get_selector('div.invalidation-history table tr td .re-validate-certificate').first.click() - self.wait_for_ajax() - - def fill_certificate_invalidation_user_name_field(self, student): # pylint: disable=invalid-name - """ - Fill username/email field with given text - """ - self.get_selector('#certificate-invalidation-user').fill(student) - - def click_invalidate_certificate_button(self): - """ - Click 'Invalidate Certificate' button in 'certificates invalidations' section - """ - self.get_selector('#invalidate-certificate').click() - - @property - def generate_certificates_button(self): - """ - Returns the "Generate Certificates" button. - """ - return self.get_selector('#btn-start-generating-certificates') - - @property - def generate_certificates_disabled_button(self): - """ - Returns the disabled state of button - """ - return self.get_selector('#disabled-btn-start-generating-certificates') - - @property - def certificate_generation_status(self): - """ - Returns certificate generation status message container. - """ - return self.get_selector('div.certificate-generation-status') - - @property - def pending_tasks_section(self): - """ - Returns the "Pending Instructor Tasks" section. - """ - return self.get_selector('div.running-tasks-container') - - @property - def certificate_exceptions_section(self): - """ - Returns the "Certificate Exceptions" section. - """ - return self.get_selector('div.certificate-exception-container') - - @property - def last_certificate_exception(self): - """ - Returns the Last Certificate Exception in Certificate Exceptions list in "Certificate Exceptions" section. - """ - return self.get_selector('div.white-listed-students table tr:last-child td') - - @property - def message(self): - """ - Returns the Message (error/success) in "Certificate Exceptions" section. - """ - return self.get_selector('.certificate-exception-container div.message') - - @property - def last_certificate_invalidation(self): - """ - Returns last certificate invalidation from "Certificate Invalidations" section. - """ - return self.get_selector('div.certificate-invalidation-container table tr:last-child td') - - @property - def certificate_invalidation_message(self): - """ - Returns the message (error/success) in "Certificate Invalidation" section. - """ - return self.get_selector('.certificate-invalidation-container div.message') - - -class EcommercePage(PageObject): - """ - E-commerce section of the Instructor dashboard. - """ - url = None - - def is_browser_on_page(self): - return self.q(css='[data-section="e-commerce"].active-section').present - - def get_sections_header_values(self): - """ - Returns a list of the headings text under div. - """ - return self.q(css="div.wrap h3").text diff --git a/common/test/acceptance/pages/lms/library.py b/common/test/acceptance/pages/lms/library.py deleted file mode 100644 index be0f297184..0000000000 --- a/common/test/acceptance/pages/lms/library.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Library Content XBlock Wrapper -""" - - -from bok_choy.page_object import PageObject - - -class LibraryContentXBlockWrapper(PageObject): - """ - A PageObject representing a wrapper around a LibraryContent block seen in the LMS - """ - url = None - BODY_SELECTOR = '.xblock-student_view div' - - def __init__(self, browser, locator): - super(LibraryContentXBlockWrapper, self).__init__(browser) - self.locator = locator - - def is_browser_on_page(self): - """ - Checks if page is opened - """ - return self.q(css='{}[data-id="{}"]'.format(self.BODY_SELECTOR, self.locator)).present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to this particular block's context - """ - return u'{}[data-id="{}"] {}'.format( - self.BODY_SELECTOR, - self.locator, - selector - ) - - @property - def children_contents(self): - """ - Gets contents of all child XBlocks as list of strings - """ - child_blocks = self.q(css=self._bounded_selector("div[data-id]")) - return frozenset(child.text for child in child_blocks) - - @property - def children_headers(self): - """ - Gets headers of all child XBlocks as list of strings - """ - child_blocks_headers = self.q(css=self._bounded_selector("div[data-id] .problem-header")) - return frozenset(child.text for child in child_blocks_headers) diff --git a/common/test/acceptance/pages/lms/login.py b/common/test/acceptance/pages/lms/login.py deleted file mode 100644 index 7d8dcc7dbb..0000000000 --- a/common/test/acceptance/pages/lms/login.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Login page for the LMS. -""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.dashboard import DashboardPage - - -class LoginPage(PageObject): - """ - Login page for the LMS. - """ - - url = BASE_URL + "/login" - - def is_browser_on_page(self): - return any([ - 'log in' in title.lower() - for title in self.q(css='span.title-super').text - ]) - - def login(self, email, password): - """ - Attempt to log in using `email` and `password`. - """ - self.provide_info(email, password) - self.submit() - - def provide_info(self, email, password): - """ - Fill in login info. - `email` and `password` are the user's credentials. - """ - EmptyPromise(self.q(css='input#email').is_present, "Click ready").fulfill() - EmptyPromise(self.q(css='input#password').is_present, "Click ready").fulfill() - - self.q(css='input#email').fill(email) - self.q(css='input#password').fill(password) - self.wait_for_ajax() - - def submit(self): - """ - Submit registration info to create an account. - """ - self.q(css='button#submit').first.click() - - # The next page is the dashboard; make sure it loads - dashboard = DashboardPage(self.browser) - dashboard.wait_for_page() - return dashboard diff --git a/common/test/acceptance/pages/lms/login_and_register.py b/common/test/acceptance/pages/lms/login_and_register.py deleted file mode 100644 index de97aefd1f..0000000000 --- a/common/test/acceptance/pages/lms/login_and_register.py +++ /dev/null @@ -1,363 +0,0 @@ -"""Login and Registration pages """ - - -from bok_choy.page_object import PageObject, unguarded -from bok_choy.promise import EmptyPromise, Promise -from six.moves.urllib.parse import urlencode - -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.dashboard import DashboardPage - - -class RegisterPage(PageObject): - """ - Registration page (create a new account) - """ - - def __init__(self, browser, course_id): - """ - Course ID is currently of the form "edx/999/2013_Spring" - but this format could change. - """ - super(RegisterPage, self).__init__(browser) - self._course_id = course_id - - @property - def url(self): - """ - URL for the registration page of a course. - """ - return "{base}/register?course_id={course_id}&enrollment_action={action}".format( - base=BASE_URL, - course_id=self._course_id, - action="enroll", - ) - - def is_browser_on_page(self): - return any([ - 'register' in title.lower() - for title in self.q(css='span.title-sub').text - ]) - - def provide_info(self, email, password, username, full_name): - """ - Fill in registration info. - `email`, `password`, `username`, and `full_name` are the user's credentials. - """ - self.wait_for_element_visibility('input#email', 'Email field is shown') - self.q(css='input#email').fill(email) - self.q(css='input#password').fill(password) - self.q(css='input#username').fill(username) - self.q(css='input#name').fill(full_name) - self.q(css='input#tos-yes').first.click() - self.q(css='input#honorcode-yes').first.click() - self.q(css="#country option[value='US']").first.click() - - def submit(self): - """ - Submit registration info to create an account. - """ - self.q(css='button#submit').first.click() - - # The next page is the dashboard; make sure it loads - dashboard = DashboardPage(self.browser) - dashboard.wait_for_page() - return dashboard - - -class ResetPasswordPage(PageObject): - """Initialize the page. - - Arguments: - browser (Browser): The browser instance. - """ - url = BASE_URL + "/login#forgot-password-modal" - - def is_browser_on_page(self): - return ( - self.q(css="#login-anchor").is_present() and - self.q(css="#password-reset-anchor").is_present() - ) - - def is_form_visible(self): - return ( - not self.q(css="#login-anchor").visible and - self.q(css="#password-reset-form").visible - ) - - def fill_password_reset_form(self, email): - """ - Fill in the form and submit it - """ - self.wait_for_element_visibility('#password-reset-email', 'Reset Email field is shown') - self.q(css="#password-reset-email").fill(email) - self.q(css="button.js-reset").click() - - def is_success_visible(self, selector): - """ - Check element is visible - """ - self.wait_for_element_visibility(selector, 'Success div is shown') - - def get_success_message(self): - """ - Return a success message displayed to the user - """ - return self.q(css=".submission-success h4").text - - -class CombinedLoginAndRegisterPage(PageObject): - """Interact with combined login and registration page. - - When enabled, the new page is available from either - `/login` or `/register`; the new page is also served at - `/account/login/` or `/account/register/`, where it was - available for a time during an A/B test. - - Users can reach this page while attempting to enroll - in a course, in which case users will be auto-enrolled - when they successfully authenticate (unless the course - has been paywalled). - - """ - def __init__(self, browser, start_page="register", course_id=None): - """Initialize the page. - - Arguments: - browser (Browser): The browser instance. - - Keyword Args: - start_page (str): Whether to start on the login or register page. - course_id (unicode): If provided, load the page as if the user - is trying to enroll in a course. - - """ - super(CombinedLoginAndRegisterPage, self).__init__(browser) - self._course_id = course_id - - if start_page not in ["register", "login"]: - raise ValueError("Start page must be either 'register' or 'login'") - self._start_page = start_page - - @property - def url(self): - """Return the URL for the combined login/registration page. """ - url = "{base}/{login_or_register}".format( - base=BASE_URL, - login_or_register=self._start_page - ) - - # These are the parameters that would be included if the user - # were trying to enroll in a course. - if self._course_id is not None: - url += "?{params}".format( - params=urlencode({ - "course_id": self._course_id, - "enrollment_action": "enroll" - }) - ) - - return url - - def is_browser_on_page(self): - """Check whether the combined login/registration page has loaded. """ - return ( - self.q(css="#login-anchor").is_present() and - self.q(css="#register-anchor").is_present() and - self.current_form is not None - ) - - def toggle_form(self): - """Toggle between the login and registration forms. """ - old_form = self.current_form - - # Toggle the form - if old_form == "login": - self.q(css=".form-toggle[data-type='register']").click() - else: - self.q(css=".form-toggle[data-type='login']").click() - - # Wait for the form to change before returning - EmptyPromise( - lambda: self.current_form != old_form, - "Finish toggling to the other form" - ).fulfill() - - def register( - self, email="", password="", username="", full_name="", country="", favorite_movie="" - ): - """Fills in and submits the registration form. - - Requires that the "register" form is visible. - This does NOT wait for the next page to load, - so the caller should wait for the next page - (or errors if that's the expected behavior.) - - Keyword Arguments: - email (unicode): The user's email address. - password (unicode): The user's password. - username (unicode): The user's username. - full_name (unicode): The user's full name. - country (unicode): Two-character country code. - - """ - # Fill in the form - self.wait_for_element_visibility('#toggle_optional_fields', 'Support education research field is shown') - if email: - self.q(css="#register-email").fill(email) - if full_name: - self.q(css="#register-name").fill(full_name) - if username: - self.q(css="#register-username").fill(username) - if password: - self.q(css="#register-password").fill(password) - if country: - self.q(css="#register-country").results[0].send_keys(country) - if favorite_movie: - self.q(css="#register-favorite_movie").fill(favorite_movie) - - # Submit it - self.q(css=".register-button").click() - - def login(self, email="", password=""): - """Fills in and submits the login form. - - Requires that the "login" form is visible. - This does NOT wait for the next page to load, - so the caller should wait for the next page - (or errors if that's the expected behavior). - - Keyword Arguments: - email (unicode): The user's email address. - password (unicode): The user's password. - - """ - # Fill in the form - self.wait_for_element_visibility('#login-email', 'Email field is shown') - self.q(css="#login-email").fill(email) - self.q(css="#login-password").fill(password) - - # Submit it - self.q(css=".login-button").click() - - def click_third_party_dummy_provider(self): - """Clicks on the Dummy third party provider login button. - - Requires that the "login" form is visible. - This does NOT wait for the ensuing page[s] to load. - Only the "Dummy" provider is used for bok choy because it is the only - one that doesn't send traffic to external servers. - """ - self.q(css="button.{}-oa2-dummy".format(self.current_form)).click() - - def password_reset(self, email): - """Navigates to, fills in, and submits the password reset form. - - Requires that the "login" form is visible. - - Keyword Arguments: - email (unicode): The user's email address. - - """ - login_form = self.current_form - - # Click the password reset link on the login page - self.q(css=".forgot-password").click() - - # Wait for the password reset form to load - EmptyPromise( - lambda: self.current_form != login_form, - "Finish toggling to the password reset form" - ).fulfill() - - # Fill in the form - self.wait_for_element_visibility('#password-reset-email', 'Email field is shown') - self.q(css="#password-reset-email").fill(email) - - # Submit it - self.q(css="button.js-reset").click() - - return CombinedLoginAndRegisterPage(self.browser).wait_for_page() - - @property - @unguarded - def current_form(self): - """Return the form that is currently visible to the user. - - Returns: - Either "register", "login", or "password-reset" if a valid - form is loaded. - - If we can't find any of these forms on the page, return None. - - """ - if self.q(css=".register-button").visible: - return "register" - elif self.q(css=".login-button").visible: - return "login" - elif self.q(css=".js-reset").visible: - return "password-reset" - elif self.q(css=".proceed-button").visible: - return "hinted-login" - - @property - def email_value(self): - """ Current value of the email form field """ - return self.q(css="#register-email").attrs('value')[0] - - @property - def full_name_value(self): - """ Current value of the full_name form field """ - return self.q(css="#register-name").attrs('value')[0] - - @property - def username_value(self): - """ Current value of the username form field """ - return self.q(css="#register-username").attrs('value')[0] - - @property - def errors(self): - """Return a list of errors displayed to the user. """ - return self.q(css=".submission-error li").text - - def wait_for_errors(self): - """Wait for errors to be visible, then return them. """ - def _check_func(): - """Return success status and any errors that occurred.""" - errors = self.errors - if not errors: - self.q(css=".register-button").click() - return (bool(errors), errors) - return Promise(_check_func, "Errors are visible").fulfill() - - @property - def success(self): - """Return a success message displayed to the user.""" - if self.q(css=".submission-success").visible: - return self.q(css=".submission-success h4").text - - def wait_for_success(self): - """Wait for a success message to be visible, then return it.""" - def _check_func(): - """Return success status and any errors that occurred.""" - success = self.success - return (bool(success), success) - return Promise(_check_func, "Success message is visible").fulfill() - - @unguarded # Because we go from this page -> temporary page -> this page again when testing the Dummy provider - def wait_for_auth_status_message(self): - """Wait for a status message to be visible following third_party registration, then return it.""" - def _check_func(): - """Return third party auth status notice message.""" - selector = '.js-auth-warning div' - msg_element = self.q(css=selector) - if msg_element.visible: - return (True, msg_element.text[0]) - return (False, None) - return Promise(_check_func, "Result of third party auth is visible").fulfill() - - @property - def hinted_login_prompt(self): - """Get the message displayed to the user on the hinted-login form""" - if self.q(css=".wrapper-other-login .instructions").visible: - return self.q(css=".wrapper-other-login .instructions").text[0] diff --git a/common/test/acceptance/pages/lms/pay_and_verify.py b/common/test/acceptance/pages/lms/pay_and_verify.py deleted file mode 100644 index 4e5109c3be..0000000000 --- a/common/test/acceptance/pages/lms/pay_and_verify.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Payment and verification pages""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import Promise - -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.dashboard import DashboardPage - - -class PaymentAndVerificationFlow(PageObject): - """Interact with the split payment and verification flow. - - The flow can be accessed at the following URLs: - `/verify_student/start-flow/{course}/` - `/verify_student/upgrade/{course}/` - `/verify_student/verify-now/{course}/` - `/verify_student/verify-later/{course}/` - `/verify_student/payment-confirmation/{course}/` - - Users can reach the flow when attempting to enroll in a course's verified - mode, either directly from the track selection page, or by upgrading from - the honor mode. Users can also reach the flow when attempting to complete - a deferred verification, or when attempting to view a receipt corresponding - to an earlier payment. - """ - def __init__(self, browser, course_id, entry_point='start-flow'): - """Initialize the page. - - Arguments: - browser (Browser): The browser instance. - course_id (unicode): The course in which the user is enrolling. - - Keyword Arguments: - entry_point (str): Where to begin the flow; must be one of 'start-flow', - 'upgrade', 'verify-now', verify-later', or 'payment-confirmation'. - - Raises: - ValueError - """ - super(PaymentAndVerificationFlow, self).__init__(browser) - self._course_id = course_id - - if entry_point not in ['start-flow', 'upgrade', 'verify-now', 'verify-later', 'payment-confirmation']: - raise ValueError( - "Entry point must be either 'start-flow', 'upgrade', 'verify-now', 'verify-later', or 'payment-confirmation'." - ) - self._entry_point = entry_point - - @property - def url(self): - """Return the URL corresponding to the initial position in the flow.""" - url = "{base}/verify_student/{entry_point}/{course}/".format( - base=BASE_URL, - entry_point=self._entry_point, - course=self._course_id - ) - - return url - - def is_browser_on_page(self): - """Check if a step in the payment and verification flow has loaded.""" - return ( - self.q(css="div .make-payment-step").is_present() or - self.q(css="div .payment-confirmation-step").is_present() or - self.q(css="div .face-photo-step").is_present() or - self.q(css="div .id-photo-step").is_present() or - self.q(css="div .review-photos-step").is_present() or - self.q(css="div .enrollment-confirmation-step").is_present() - ) - - def indicate_contribution(self): - """Interact with the radio buttons appearing on the first page of the upgrade flow.""" - self.q(css=".contribution-option > input").first.click() - - def immediate_verification(self): - """Interact with the immediate verification button.""" - self.q(css="#verify_now_button").click() - - PaymentAndVerificationFlow(self.browser, self._course_id, entry_point='verify-now').wait_for_page() - - def defer_verification(self): - """Interact with the link allowing the user to defer their verification.""" - self.q(css="#verify_later_button").click() - - DashboardPage(self.browser).wait_for_page() - - def webcam_capture(self): - """Interact with a webcam capture button.""" - self.q(css="#webcam_capture_button").click() - - def _check_func(): - next_step_button_classes = self.q(css="#next_step_button").attrs('class') - next_step_button_enabled = 'is-disabled' not in next_step_button_classes - return (next_step_button_enabled, next_step_button_classes) - - # Check that the #next_step_button is enabled before returning control to the caller - Promise(_check_func, "The 'Next Step' button is enabled.").fulfill() - - def next_verification_step(self, next_page_object): - """Interact with the 'Next' step button found in the verification flow.""" - self.q(css="#next_step_button").click() - - next_page_object.wait_for_page() - - def go_to_dashboard(self): - """Interact with the link to the dashboard appearing on the enrollment confirmation page.""" - if self.q(css="div .enrollment-confirmation-step").is_present(): - self.q(css=".action-primary").click() - else: - raise Exception("The dashboard can only be accessed from the enrollment confirmation.") - - DashboardPage(self.browser).wait_for_page() diff --git a/common/test/acceptance/pages/lms/problem.py b/common/test/acceptance/pages/lms/problem.py index 6cab7bba62..c622c9e2f7 100644 --- a/common/test/acceptance/pages/lms/problem.py +++ b/common/test/acceptance/pages/lms/problem.py @@ -4,8 +4,6 @@ Problem Page. from bok_choy.page_object import PageObject -from selenium.webdriver import ActionChains -from selenium.webdriver.common.keys import Keys from common.test.acceptance.pages.common.utils import click_css @@ -36,387 +34,11 @@ class ProblemPage(PageObject): self.wait_for_element_visibility(self.CSS_PROBLEM_HEADER, 'wait for problem header') return self.q(css='.problem-header').text[0] - @property - def problem_text(self): - """ - Return the text of the question of the problem. - """ - return self.q(css="div.problem p").text - - @property - def problem_input_content(self): - """ - Return the text of the question of the problem. - """ - return self.q(css="div.wrapper-problem-response").text[0] - - @property - def problem_content(self): - """ - Return the content of the problem - """ - return self.q(css="div.problems-wrapper").text[0] - - @property - def problem_meta(self): - """ - Return the problem meta text - """ - return self.q(css=".problems-wrapper .problem-progress").text[0] - - @property - def message_text(self): - """ - Return the "message" text of the question of the problem. - """ - return self.q(css="div.problem span.message").text[0] - - @property - def extract_hint_text_from_html(self): - """ - Return the "hint" text of the problem from html - """ - hints_html = self.q(css="div.problem .notification-hint .notification-message li").html - return [hint_html.split(' ol > li.hint-index-{hint_index}'.format( - hint_index=hint_index - ) - self.wait_for( - lambda: self.q(css=css).focused, - 'Waiting for the focus to be on the hint notification' - ) - - def click_review_in_notification(self, notification_type): - """ - Click on the "Review" button within the visible notification. - """ - css_string = u'.notification.notification-{notification_type} .review-btn'.format( - notification_type=notification_type - ) - - # The review button cannot be clicked on until it is tabbed to, so first tab to it. - # Multiple tabs may be required depending on the content (for instance, hints with links). - def tab_until_review_focused(): - """ Tab until the review button is focused """ - self.browser.switch_to_active_element().send_keys(Keys.TAB) - if self.q(css=css_string).focused: - self.scroll_to_element(css_string) - return self.q(css=css_string).focused - - self.wait_for( - tab_until_review_focused, - 'Waiting for the Review button to become focused' - ) - self.wait_for_element_visibility( - css_string, - 'Waiting for the button to be visible' - ) - click_css(self, css_string, require_notification=False) - - def get_hint_button_disabled_attr(self): - """ Return the disabled attribute of all hint buttons (once hints are visible, there will be two). """ - return self.q(css='.problem .hint-button').attrs('disabled') + click_css(self, '.problem .submit') def click_choice(self, choice_value): """ @@ -424,218 +46,3 @@ class ProblemPage(PageObject): """ self.q(css='div.problem .choicegroup input[value="' + choice_value + '"]').first.click() self.wait_for_ajax() - - def is_correct(self): - """ - Is there a "correct" status showing? - """ - return self.q(css="div.problem div.capa_inputtype.textline div.correct span.status").is_present() - - def simpleprob_is_correct(self): - """ - Is there a "correct" status showing? Works with simple problem types. - """ - return self.q(css="div.problem div.inputtype div.correct span.status").is_present() - - def simpleprob_is_partially_correct(self): - """ - Is there a "partially correct" status showing? Works with simple problem types. - """ - return self.q(css="div.problem div.inputtype div.partially-correct span.status").is_present() - - def simpleprob_is_incorrect(self): - """ - Is there an "incorrect" status showing? Works with simple problem types. - """ - return self.q(css="div.problem div.inputtype div.incorrect span.status").is_present() - - def get_simpleprob_correctness(self): - """ - Returns the correctness status for a simple problem. - - Given a simple problem, the method returns the correctness status. - If there is no visible status, None is returned - """ - - if self.simpleprob_is_correct(): - return 'correct' - elif self.simpleprob_is_incorrect(): - return 'incorrect' - elif self.simpleprob_is_partially_correct(): - return 'partial' - else: - return None - - def click_clarification(self, index=0): - """ - Click on an inline icon that can be included in problem text using an HTML element: - - Problem clarification text hidden by an icon in rendering Text - """ - self.q(css=u'div.problem .clarification:nth-child({index}) span[data-tooltip]'.format(index=index + 1)).click() - - @property - def visible_tooltip_text(self): - """ - Get the text seen in any tooltip currently visible on the page. - """ - self.wait_for_element_visibility('body > .tooltip', 'A tooltip is visible.') - return self.q(css='body > .tooltip').text[0] - - def is_solution_tag_present(self): - """ - Check if solution/explanation is shown. - """ - solution_selector = '.solution-span div.detailed-solution' - return self.q(css=solution_selector).is_present() - - def is_choice_highlighted(self, choice, choices_list, show_answer=True): - """ - Check if the given answer/choice is highlighted for choice group. - - show_answer: if set, then requires each choice to be marked with a status. - If not set, then the status can be elswhere in the problem. - """ - if show_answer: - choice_status_xpath = (u'//fieldset/div[contains(@class, "field")][{{0}}]' - u'/label[contains(@class, "choicegroup_{choice}")]' - u'/span[contains(@class, "status {choice}")]'.format(choice=choice)) - any_status_xpath = u'//fieldset/div[contains(@class, "field")][{0}]/label/span' - else: - choice_status_xpath = (u'//fieldset/div[contains(@class, "field")][{{0}}]' - u'/label[contains(@class, "choicegroup_{choice}")]'.format(choice=choice)) - any_status_xpath = u'//div[contains(@class, "indicator-container")]/span[contains(@class, "status")]' - - for possible_choice in choices_list: - if not self.q(xpath=choice_status_xpath.format(possible_choice)).is_present(): - return False - - # Check that there is only a single status span, as there were some bugs with multiple - # spans (with various classes) being appended. - if not len(self.q(xpath=any_status_xpath.format(possible_choice)).results) == 1: - return False - - return True - - def is_correct_choice_highlighted(self, correct_choices, show_answer=True): - """ - Check if correct answer/choice highlighted for choice group. - """ - return self.is_choice_highlighted('correct', correct_choices, show_answer) - - def is_submitted_choice_highlighted(self, correct_choices): - """ - Check if submitted answer/choice highlighted for choice group. - """ - return self.is_choice_highlighted('submitted', correct_choices) - - @property - def problem_question(self): - """ - Return the question text of the problem. - """ - return self.q(css="div.problem .wrapper-problem-response legend").text[0] - - @property - def problem_question_descriptions(self): - """ - Return a list of question descriptions of the problem. - """ - return self.q(css="div.problem .wrapper-problem-response .question-description").text - - @property - def problem_progress_graded_value(self): - """ - Return problem progress text which contains weight of problem, if it is graded, and the student's current score. - """ - self.wait_for_element_visibility('.problem-progress', "Problem progress is visible") - return self.q(css='.problem-progress').text[0] - - @property - def status_sr_text(self): - """ - Returns the text in the special "sr" region used for display status. - """ - return self.q(css='#reader-feedback').text[0] - - @property - def submission_feedback(self): - """ - Returns the submission feedback of the problem - """ - return self.q(css='div[class="submission-feedback"]').text[0].split('\n')[0] - - @property - def answer(self): - """ - Returns the answer of the problem - """ - return self.q(css='p[class="answer"]').text[0] - - @property - def score_notification(self): - """ - Returns the score after the submission of answer - """ - self.wait_for_element_visibility('.notification-submit .notification-message', 'Problem score is visible') - return self.q(css='.notification-submit .notification-message').text[0] - - def is_present(self, selector): - """ - Checks for the presence of the locator - """ - return self.q(css=selector).present - - -class DragAndDropPage(PageObject): - """ - View for a Drag & Drop problem. - """ - - url = None - - def is_browser_on_page(self): - return self.q(css='.xblock-student_view').present - - def is_submit_disabled(self): - """ - Checks if the submit button is disabled for Drag & Drop problem. - """ - disabled_attr = self.q(css='.submit-answer-button').attrs('disabled')[0] - return disabled_attr == 'true' - - def is_present(self, selector): - """ - Checks for the presence of the locator. - """ - return self.q(css=selector).present - - def is_submit_button_present(self): - """ - Verifies if the submit button is present for DnD problems - with assessment mode. - """ - return self.is_present('.submit-answer-button') - - def _get_item_by_value(self, item_value): - """ - Get the item that will be placed onto a zone. - """ - return self.q(xpath=(".//div[@data-value='{item_id}']".format(item_id=item_value)))[0] - - def _get_zone_by_id(self, zone_id): - """ - Get zone where the item will be placed. - """ - zones_container = self.browser.find_element_by_css_selector('.target') - return zones_container.find_elements_by_xpath(".//div[@data-uid='{zone_id}']".format(zone_id=zone_id))[0] - - def drag_item_to_zone(self, item_value, zone_id): - """ - Drag item to desired zone using mouse interaction. - """ - element = self._get_item_by_value(item_value) - target = self._get_zone_by_id(zone_id) - action_chains = ActionChains(self.browser) - action_chains.drag_and_drop(element, target).perform() - self.wait_for_ajax() diff --git a/common/test/acceptance/pages/lms/progress.py b/common/test/acceptance/pages/lms/progress.py index 2fe3c0c019..f6a81f55a6 100644 --- a/common/test/acceptance/pages/lms/progress.py +++ b/common/test/acceptance/pages/lms/progress.py @@ -3,8 +3,6 @@ Student progress page """ -from six.moves import map - from common.test.acceptance.pages.lms.course_page import CoursePage @@ -22,61 +20,14 @@ class ProgressPage(CoursePage): ) return is_present - @property - def grading_formats(self): - return [label.replace(' Scores:', '') for label in self.q(css=".scores dt").text] - - def section_score(self, chapter, section): + def x_tick_sr_text(self, tick_index): """ - Return a list of (points, max_points) tuples representing the - aggregate score for the section. - - Example: - page.section_score('Week 1', 'Lesson 1') --> (2, 5) - - Returns `None` if no such chapter and section can be found. + Return an array of the sr text for a specific x-Axis tick on the + progress chart. """ - # Find the index of the section in the chapter - chapter_index = self._chapter_index(chapter) - if chapter_index is None: - return None - - section_index = self._section_index(chapter_index, section) - if section_index is None: - return None - - # Retrieve the scores for the section - return self._aggregate_section_score(chapter_index, section_index) - - def scores(self, chapter, section): - """ - Return a list of (points, max_points) tuples representing the scores - for the section. - - Example: - page.scores('Week 1', 'Lesson 1') --> [(2, 4), (0, 1)] - - Returns `None` if no such chapter and section can be found. - """ - - # Find the index of the section in the chapter - chapter_index = self._chapter_index(chapter) - if chapter_index is None: - return None - - section_index = self._section_index(chapter_index, section) - if section_index is None: - return None - - # Retrieve the scores for the section - return self._section_scores(chapter_index, section_index) - - def text_on_page(self, text): - """ - Return whether the given text appears - on the page. - """ - return text in self.q(css=".view-in-course").html[0] + selector = self.q(css='#grade-detail-graph .tickLabel')[tick_index] + sr_fields = selector.find_elements_by_class_name('sr') + return [field.text for field in sr_fields] def x_tick_label(self, tick_index): """ @@ -87,15 +38,6 @@ class ProgressPage(CoursePage): tick_label = selector.find_elements_by_tag_name('span')[0] return [tick_label.text, tick_label.get_attribute('aria-hidden')] - def x_tick_sr_text(self, tick_index): - """ - Return an array of the sr text for a specific x-Axis tick on the - progress chart. - """ - selector = self.q(css='#grade-detail-graph .tickLabel')[tick_index] - sr_fields = selector.find_elements_by_class_name('sr') - return [field.text for field in sr_fields] - def y_tick_label(self, tick_index): """ Returns the label for the Y-axis tick index, @@ -113,85 +55,3 @@ class ProgressPage(CoursePage): selector = self.q(css='#grade-detail-graph .overallGrade')[0] label = selector.find_elements_by_class_name('sr')[0] return [label.text, selector.text] - - def _chapter_index(self, title): - """ - Return the CSS index of the chapter with `title`. - Returns `None` if it cannot find such a chapter. - """ - chapter_css = '.chapters>section h3' - chapter_titles = self.q(css=chapter_css).map(lambda el: el.text.lower().strip()).results - - try: - # CSS indices are 1-indexed, so add one to the list index - return chapter_titles.index(title.lower()) + 1 - except ValueError: - self.warning(u"Could not find chapter '{0}'".format(title)) - return None - - def _section_index(self, chapter_index, title): - """ - Return the CSS index of the section with `title` in the chapter at `chapter_index`. - Returns `None` if it can't find such a section. - """ - - # This is a hideous CSS selector that means: - # Get the links containing the section titles in `chapter_index`. - # The link text is the section title. - section_css = u'.chapters>section:nth-of-type({0}) .sections div .hd a'.format(chapter_index) - section_titles = self.q(css=section_css).map(lambda el: el.text.lower().strip()).results - - # The section titles also contain "n of m possible points" on the second line - # We have to remove this to find the right title - section_titles = [t.split('\n')[0] for t in section_titles] - - # Some links are blank, so remove them - section_titles = [t for t in section_titles if t] - - try: - # CSS indices are 1-indexed, so add one to the list index - return section_titles.index(title.lower()) + 1 - except ValueError: - self.warning(u"Could not find section '{0}'".format(title)) - return None - - def _aggregate_section_score(self, chapter_index, section_index): - """ - Return a tuple of the form `(points, max_points)` representing - the aggregate score for the specified chapter and section. - """ - score_css = u".chapters>section:nth-of-type({0}) .sections>div:nth-of-type({1}) .hd>span".format( - chapter_index, section_index - - ) - text_scores = self.q(css=score_css).text - assert len(text_scores) == 1 - text_score = text_scores[0] - text_score = text_score.split()[0] # strip off percentage, if present - - assert (text_score[0], text_score[-1]) == ('(', ')') - text_score = text_score.strip('()') - - assert '/' in text_score - score = tuple(int(x) for x in text_score.split('/')) - assert len(score) == 2 - return score - - def _section_scores(self, chapter_index, section_index): - """ - Return a list of `(points, max_points)` tuples representing - the scores in the specified chapter and section. - - `chapter_index` and `section_index` start at 1. - """ - # This is CSS selector means: - # Get the scores for the chapter at `chapter_index` and the section at `section_index` - # Example text of the retrieved elements: "0/1" - score_css = u".chapters>section:nth-of-type({0}) .sections>div:nth-of-type({1}) .scores>dd".format( - chapter_index, section_index - ) - - text_scores = self.q(css=score_css).text - - # Convert text scores to tuples of (points, max_points) - return [tuple(map(int, score.split('/'))) for score in text_scores] diff --git a/common/test/acceptance/pages/lms/staff_view.py b/common/test/acceptance/pages/lms/staff_view.py index de5f312723..630e00af93 100644 --- a/common/test/acceptance/pages/lms/staff_view.py +++ b/common/test/acceptance/pages/lms/staff_view.py @@ -43,26 +43,6 @@ class StaffPreviewPage(PageObject): """ return self.q(css=self.VIEW_MODE_OPTIONS_CSS).filter(lambda el: el.is_selected()).first.text[0] - def set_staff_view_mode(self, view_mode): - """ - Set the current view mode, e.g. "Staff", "Learner" or a content group. - """ - self.q(css=self.VIEW_MODE_OPTIONS_CSS).filter(lambda el: el.text.strip() == view_mode).first.click() - self.wait_for_ajax() - - def set_staff_view_mode_specific_student(self, username_or_email): - """ - Set the current preview mode to "Specific learner" with the given username or email - """ - required_mode = "Specific learner" - if self.staff_view_mode != required_mode: - self.q(css=self.VIEW_MODE_OPTIONS_CSS).filter(lambda el: el.text == required_mode).first.click() - # Use a script here because .clear() + .send_keys() triggers unwanted behavior if a username is already set - self.browser.execute_script( - '$(".action-preview-username").val("{}").blur().change();'.format(username_or_email) - ) - self.wait_for_ajax() - class StaffCoursewarePage(CoursewarePage, StaffPreviewPage): """ @@ -79,81 +59,3 @@ class StaffCoursewarePage(CoursewarePage, StaffPreviewPage): if not CoursewarePage.is_browser_on_page(self): return False return StaffPreviewPage.is_browser_on_page(self) - - def open_staff_debug_info(self): - """ - Open the staff debug window - Return the page object for it. - """ - self.q(css='a.instructor-info-action').first.click() - staff_debug_page = StaffDebugPage(self.browser) - staff_debug_page.wait_for_page() - return staff_debug_page - - def answer_problem(self): - """ - Answers the problem to give state that we can clean - """ - self.q(css='input.check').first.click() - self.wait_for_ajax() - - def load_problem_via_ajax(self): - """ - Load problem via ajax by clicking next. - """ - self.q(css="li.next").click() - self.wait_for_ajax() - - -class StaffDebugPage(PageObject): - """ - Staff Debug modal - """ - - url = None - - def is_browser_on_page(self): - return self.q(css='.staff-modal').present - - def reset_attempts(self, user=None): - """ - This clicks on the reset attempts link with an optionally - specified user. - """ - if user: - self.q(css='input[id^=sd_fu_]').first.fill(user) - self.q(css='.staff-modal .staff-debug-reset').click() - - def delete_state(self, user=None): - """ - This delete's a student's state for the problem - """ - if user: - self.q(css='input[id^=sd_fu_]').first.fill(user) - self.q(css='.staff-modal .staff-debug-sdelete').first.click() - - def rescore(self, user=None): - """ - This clicks on the reset attempts link with an optionally - specified user. - """ - if user: - self.q(css='input[id^=sd_fu_]').first.fill(user) - self.q(css='.staff-modal .staff-debug-rescore').click() - - def rescore_if_higher(self, user=None): - """ - This clicks on the reset attempts link with an optionally - specified user. - """ - if user: - self.q(css='input[id^=sd_fu_]').first.fill(user) - self.q(css='.staff-modal .staff-debug-rescore-if-higher').click() - - @property - def idash_msg(self): - """ - Returns the value of #idash_msg - """ - self.wait_for_ajax() - return self.q(css='#idash_msg').text diff --git a/common/test/acceptance/pages/lms/tab_nav.py b/common/test/acceptance/pages/lms/tab_nav.py index 99dadcb826..7fcf7ecd91 100644 --- a/common/test/acceptance/pages/lms/tab_nav.py +++ b/common/test/acceptance/pages/lms/tab_nav.py @@ -14,8 +14,8 @@ class TabNavPage(PageObject): url = None - def is_using_v1_style_tabs(self): - return self.q(css='ol.course-tabs').present + # def is_using_v1_style_tabs(self): + # return self.q(css='ol.course-tabs').present def is_using_boostrap_style_tabs(self): return self.q(css='ul.navbar-nav').present @@ -44,24 +44,6 @@ class TabNavPage(PageObject): self.wait_for_page() self._is_on_tab_promise(tab_name).fulfill() - def mathjax_has_rendered(self): - """ - Check that MathJax has rendered in tab content - """ - mathjax_container = self.q(css=".static_tab_wrapper .MathJax") - EmptyPromise( - lambda: mathjax_container.present and mathjax_container.visible, - "MathJax is not visible" - ).fulfill() - - def is_on_tab(self, tab_name): - """ - Return a boolean indicating whether the current tab is `tab_name`. - Because this is a public method, it checks that we're on the right page - before accessing the DOM. - """ - return self._is_on_tab(tab_name) - def _tab_css(self, tab_name): """ Return the CSS to click for `tab_name`. @@ -125,10 +107,3 @@ class TabNavPage(PageObject): lambda: self._is_on_tab(tab_name), u"{0} is the current tab".format(tab_name) ) - - def has_new_post_button_visible_on_tab(self): - """ - Check if new post button present and visible on course tab page - """ - new_post_btn = self.q(css='ol.course-tabs .new-post-btn') - return new_post_btn.present and new_post_btn.visible diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py deleted file mode 100644 index 195cde1001..0000000000 --- a/common/test/acceptance/pages/lms/teams.py +++ /dev/null @@ -1,602 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Teams pages. -""" - - -from common.test.acceptance.pages.common.paging import PaginatedUIMixin -from common.test.acceptance.pages.common.utils import click_css, confirm_prompt -from common.test.acceptance.pages.lms.course_page import CoursePage -from common.test.acceptance.pages.lms.discussion import InlineDiscussionPage -from common.test.acceptance.pages.lms.fields import FieldsMixin - -TOPIC_CARD_CSS = 'div.wrapper-card-core' -CARD_TITLE_CSS = 'h3.card-title' -MY_TEAMS_BUTTON_CSS = '.nav-item[data-index="0"]' -BROWSE_BUTTON_CSS = '.nav-item[data-index="1"]' -TEAMS_LINK_CSS = '.action-view' -TEAMS_HEADER_CSS = '.teams-header' -CREATE_TEAM_LINK_CSS = '.create-team' - - -class TeamCardsMixin(object): - """Provides common operations on the team card component.""" - - def _bounded_selector(self, css): - """Bind the CSS to a particular tabpanel (e.g. My Teams or Browse).""" - return u'{tabpanel_id} {css}'.format(tabpanel_id=getattr(self, 'tabpanel_id', ''), css=css) - - def view_first_team(self): - """Click the 'view' button of the first team card on the page.""" - self.q(css=self._bounded_selector('a.action-view')).first.click() - - @property - def team_cards(self): - """Get all the team cards on the page.""" - return self.q(css=self._bounded_selector('.team-card')) - - @property - def team_names(self): - """Return the names of each team on the page.""" - return self.q(css=self._bounded_selector('h3.card-title')).map(lambda e: e.text).results - - @property - def team_descriptions(self): - """Return the names of each team on the page.""" - return self.q(css=self._bounded_selector('p.card-description')).map(lambda e: e.text).results - - @property - def team_memberships(self): - """Return the team memberships text for each card on the page.""" - return self.q(css=self._bounded_selector('.member-count')).map(lambda e: e.text).results - - -class BreadcrumbsMixin(object): - """Provides common operations on teams page breadcrumb links.""" - - @property - def header_page_breadcrumbs(self): - """Get the page breadcrumb text displayed by the page header""" - return self.q(css='.page-header .breadcrumbs')[0].text - - def click_all_topics(self): - """ Click on the "All Topics" breadcrumb """ - self.q(css='a.nav-item').filter(text='All Topics')[0].click() - - def click_specific_topic(self, topic): - """ Click on the breadcrumb for a specific topic """ - self.q(css='a.nav-item').filter(text=topic)[0].click() - - -class TeamsPage(CoursePage, BreadcrumbsMixin): - """ - Teams page/tab. - """ - url_path = "teams" - - def is_browser_on_page(self): - """ Checks if teams page is being viewed """ - return self.q(css='body.view-teams').present - - def get_body_text(self): - """ Returns the current dummy text. This will be changed once there is more content on the page. """ - main_page_content_css = '.page-content-main' - self.wait_for( - lambda: len(self.q(css=main_page_content_css).text) == 1, - description="Body text is present" - ) - return self.q(css=main_page_content_css).text[0] - - def active_tab(self): - """ Get the active tab. """ - return self.q(css='.is-active').attrs('data-url')[0] - - def browse_topics(self): - """ View the Browse tab of the Teams page. """ - self.q(css=BROWSE_BUTTON_CSS).click() - - def verify_team_count_in_first_topic(self, expected_count): - """ - Verify that the team count on the first topic card in the topic list is correct - (browse topics page). - """ - self.wait_for( - lambda: self.q(css='.team-count')[0].text == "0 Teams" if expected_count == 0 else "1 Team", - description="Team count text on topic is wrong" - ) - - def verify_topic_team_count(self, expected_count): - """ Verify the number of teams listed on the topic page (browse teams within topic). """ - self.wait_for( - lambda: len(self.q(css='.team-card')) == expected_count, - description="Expected number of teams is wrong" - ) - - def verify_my_team_count(self, expected_count): - """ Verify the number of teams on 'My Team'. """ - - # Click to "My Team" and verify that it contains the expected number of teams. - self.q(css=MY_TEAMS_BUTTON_CSS).click() - self.wait_for_ajax() - self.wait_for( - lambda: len(self.q(css='.team-card')) == expected_count, - description="Expected number of teams is wrong" - ) - - def click_all_topics(self): - """ Click on the "All Topics" breadcrumb """ - self.q(css='a.nav-item').filter(text='All Topics')[0].click() - - def click_specific_topic(self, topic): - """ Click on the breadcrumb for a specific topic """ - self.q(css='a.nav-item').filter(text=topic)[0].click() - - @property - def warning_message(self): - """Return the text of the team warning message.""" - return self.q(css='.warning').results[0].text - - -class MyTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin): - """ - The 'My Teams' tab of the Teams page. - """ - - url_path = "teams/#my-teams" - tabpanel_id = '#tabpanel-my-teams' - - def is_browser_on_page(self): - """Check if the "My Teams" tab is being viewed.""" - button_classes = self.q(css=MY_TEAMS_BUTTON_CSS).attrs('class') - if len(button_classes) == 0: - return False - return 'is-active' in button_classes[0] - - -class BrowseTopicsPage(CoursePage, PaginatedUIMixin): - """ - The 'Browse' tab of the Teams page. - """ - - url_path = "teams/#browse" - - def is_browser_on_page(self): - """Check if the Browse tab is being viewed.""" - # First off, you need to make sure that you're on the Teams page. - if not self.q(css='.teams-main').visible: - return False - button_classes = self.q(css=BROWSE_BUTTON_CSS).attrs('class') - if len(button_classes) == 0: - return False - return 'is-active' in button_classes[0] - - @property - def topic_cards(self): - """Return a list of the topic cards present on the page.""" - return self.q(css=TOPIC_CARD_CSS).results - - @property - def topic_names(self): - """Return a list of the topic names present on the page.""" - return self.q(css='#tabpanel-browse ' + CARD_TITLE_CSS).map(lambda e: e.text).results - - @property - def topic_descriptions(self): - """Return a list of the topic descriptions present on the page.""" - return self.q(css='p.card-description').map(lambda e: e.text).results - - def browse_teams_for_topic(self, topic_name): - """ - Show the teams list for `topic_name`. - """ - self.q(css=TEAMS_LINK_CSS).filter( - text=u'View Teams in the {topic_name} Topic'.format(topic_name=topic_name) - )[0].click() - self.wait_for_ajax() - - def sort_topics_by(self, sort_order): - """Sort the list of topics by the given `sort_order`.""" - self.q( - css=u'#paging-header-select option[value={sort_order}]'.format(sort_order=sort_order) - ).click() - self.wait_for_ajax() - - -class BaseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin, BreadcrumbsMixin): - """ - The paginated UI for browsing teams within a Topic on the Teams - page. - """ - def __init__(self, browser, course_id, topic): - """ - Note that `topic` is a dict representation of a topic following - the same convention as a course module's topic. - """ - super(BaseTeamsPage, self).__init__(browser, course_id) - self.browser = browser - self.course_id = course_id - self.topic = topic - - def is_browser_on_page(self): - """Check if we're on a teams list page for a particular topic.""" - has_correct_url = self.url.endswith(self.url_path) - teams_list_view_present = self.q(css='.teams-main').present - return has_correct_url and teams_list_view_present - - @property - def header_name(self): - """Get the topic name displayed by the page header""" - return self.q(css=TEAMS_HEADER_CSS + ' .page-title')[0].text - - @property - def header_description(self): - """Get the topic description displayed by the page header""" - return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text - - @property - def sort_order(self): - """Return the current sort order on the page.""" - return self.q( - css='#paging-header-select option' - ).filter( - lambda e: e.is_selected() - ).results[0].text.strip() - - @property - def team_names(self): - """Get all the team names on the page.""" - return self.q(css=CARD_TITLE_CSS).map(lambda e: e.text).results - - def click_create_team_link(self): - """ Click on create team link.""" - query = self.q(css=CREATE_TEAM_LINK_CSS) - if query.present: - query.first.click() - - # This will bring you to the team management page - team_management_page = TeamManagementPage(self.browser, self.course_id, self.topic) - team_management_page.wait_for_page() - - def click_search_team_link(self): - """ Click on create team link.""" - query = self.q(css='.search-team-descriptions') - if query.present: - query.first.click() - self.wait_for_ajax() - - def click_browse_all_teams_link(self): - """ Click on browse team link.""" - query = self.q(css='.browse-teams') - if query.present: - query.first.click() - self.wait_for_ajax() - - def sort_teams_by(self, sort_order): - """Sort the list of teams by the given `sort_order`.""" - self.q( - css=u'#paging-header-select option[value={sort_order}]'.format(sort_order=sort_order) - ).click() - self.wait_for_ajax() - - @property - def _showing_search_results(self): - """ - Returns true if showing search results. - """ - return self.header_description.startswith(u"Showing results for") - - def search(self, string): - """ - Searches for the specified string, and returns a SearchTeamsPage - representing the search results page. - """ - self.q(css='.search-field').first.fill(string) - self.q(css='.action-search').first.click() - self.wait_for_ajax() - self.wait_for( - lambda: self._showing_search_results, - description="Showing search results" - ) - page = SearchTeamsPage(self.browser, self.course_id, self.topic) - page.wait_for_page() - return page - - -class BrowseTeamsPage(BaseTeamsPage): - """ - The paginated UI for browsing teams within a Topic on the Teams - page. - """ - def __init__(self, browser, course_id, topic): - super(BrowseTeamsPage, self).__init__(browser, course_id, topic) - self.url_path = "teams/#topics/{topic_id}".format(topic_id=self.topic['id']) - - -class SearchTeamsPage(BaseTeamsPage): - """ - The paginated UI for showing team search results. - page. - """ - def __init__(self, browser, course_id, topic): - super(SearchTeamsPage, self).__init__(browser, course_id, topic) - self.url_path = "teams/#topics/{topic_id}/search".format(topic_id=self.topic['id']) - - -class TeamManagementPage(CoursePage, FieldsMixin, BreadcrumbsMixin): - """ - Team page for creation, editing, and deletion. - """ - def __init__(self, browser, course_id, topic): - """ - Set up `self.url_path` on instantiation, since it dynamically - reflects the current topic. Note that `topic` is a dict - representation of a topic following the same convention as a - course module's topic. - """ - super(TeamManagementPage, self).__init__(browser, course_id) - self.topic = topic - self.url_path = "teams/#topics/{topic_id}/create-team".format(topic_id=self.topic['id']) - - def is_browser_on_page(self): - """Check if we're on the create team page for a particular topic.""" - fields_css = '.team-edit-fields' - button_sr_css = '.action.action-primary > .sr' - return self.q(css=fields_css).present and self.q(css=button_sr_css).visible - - @property - def header_page_name(self): - """Get the page name displayed by the page header""" - return self.q(css='.page-header .page-title')[0].text - - @property - def header_page_description(self): - """Get the page description displayed by the page header""" - return self.q(css='.page-header .page-description')[0].text - - @property - def validation_message_text(self): - """Get the error message text""" - return self.q(css='.create-team.wrapper-msg .copy')[0].text - - def create_team(self, name='Team Name', description='Team description.'): - """Create a new team""" - self.value_for_text_field(field_id='name', value=name, press_enter=False) - self.set_value_for_textarea_field( - field_id='description', - value=description - ) - self.submit_form() - - def submit_form(self): - """Click on create team button""" - self.q(css='.create-team .action-primary').first.click() - self.wait_for_ajax() - - def cancel_team(self): - """Click on cancel team button""" - self.q(css='.create-team .action-cancel').first.click() - self.wait_for_ajax() - - @property - def delete_team_button(self): - """Returns the 'delete team' button.""" - return self.q(css='.action-delete').first - - def click_membership_button(self): - """Clicks the 'edit membership' button""" - self.q(css='.action-edit-members').first.click() - self.wait_for_ajax() - - @property - def membership_button_present(self): - """Checks if the edit membership button is present""" - return self.q(css='.action-edit-members').present - - -class EditMembershipPage(CoursePage): - """ - Staff or discussion-privileged user page to remove troublesome or inactive - students from a team - """ - def __init__(self, browser, course_id, team): - """ - Set up `self.url_path` on instantiation, since it dynamically - reflects the current team. - """ - super(EditMembershipPage, self).__init__(browser, course_id) - self.team = team - self.url_path = "teams/#teams/{topic_id}/{team_id}/edit-team/manage-members".format( - topic_id=self.team['topic_id'], team_id=self.team['id'] - ) - - def is_browser_on_page(self): - """Check if we're on the team membership page for a particular team.""" - self.wait_for_ajax() - - if self.q(css='.edit-members').present: - return True - empty_query = self.q(css='.teams-main>.page-content>p').first - return ( - len(empty_query.results) > 0 and - empty_query[0].text == "This team does not have any members." - ) - - @property - def team_members(self): - """Returns the number of team members shown on the page.""" - return len(self.q(css='.team-member')) - - def click_first_remove(self): - """Clicks the remove link on the first member listed.""" - self.q(css='.action-remove-member').first.click() - - def confirm_delete_membership_dialog(self): - """Click 'delete' on the warning dialog.""" - confirm_prompt(self, require_notification=False) - self.wait_for_ajax() - - def cancel_delete_membership_dialog(self): - """Click 'delete' on the warning dialog.""" - confirm_prompt(self, cancel=True) - - -class TeamPage(CoursePage, PaginatedUIMixin, BreadcrumbsMixin): - """ - The page for a specific Team within the Teams tab - """ - def __init__(self, browser, course_id, team=None): - """ - Set up `self.url_path` on instantiation, since it dynamically - reflects the current team. - """ - super(TeamPage, self).__init__(browser, course_id) - self.team = team - if self.team: - self.url_path = "teams/#teams/{topic_id}/{team_id}".format( - topic_id=self.team['topic_id'], team_id=self.team['id'] - ) - - def is_browser_on_page(self): - """Check if we're on the teams list page for a particular team.""" - self.wait_for_ajax() - if self.team: - if not self.url.endswith(self.url_path): - return False - return self.q(css='.teams-main .team-members').visible - - @property - def discussion_id(self): - """Get the id of the discussion module on the page""" - return self.q(css='div.discussion-module').attrs('data-discussion-id')[0] - - @property - def discussion_page(self): - """Get the discussion as a bok_choy page object""" - if not hasattr(self, '_discussion_page'): - # pylint: disable=attribute-defined-outside-init - self._discussion_page = InlineDiscussionPage(self.browser, self.discussion_id) - return self._discussion_page - - @property - def team_name(self): - """Get the team's name as displayed in the page header""" - return self.q(css='.page-header .page-title')[0].text - - @property - def team_description(self): - """Get the team's description as displayed in the page header""" - return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text - - @property - def team_members_present(self): - """Verifies that team members are present""" - return self.q(css='.page-content-secondary .team-members .team-member').present - - @property - def team_capacity_text(self): - """Returns team capacity text""" - return self.q(css='.page-content-secondary .team-capacity :last-child').text[0] - - @property - def team_location(self): - """ Returns team location/country. """ - return self.q(css='.page-content-secondary .team-country :last-child').text[0] - - @property - def team_language(self): - """ Returns team location/country. """ - return self.q(css='.page-content-secondary .team-language :last-child').text[0] - - @property - def team_user_membership_text(self): - """Returns the team membership text""" - query = self.q(css='.page-content-secondary > .team-user-membership-status') - return query.text[0] if query.present else '' - - @property - def team_leave_link_present(self): - """Verifies that team leave link is present""" - return self.q(css='.leave-team-link').present - - def click_leave_team_link(self, remaining_members=0, cancel=False): - """ Click on Leave Team link""" - leave_team_css = '.leave-team-link' - self.scroll_to_element(leave_team_css) - self.wait_for_element_visibility(leave_team_css, 'Leave Team link is visible.') - click_css(self, leave_team_css, require_notification=False) - confirm_prompt(self, cancel, require_notification=False) - - if cancel is False: - self.wait_for( - lambda: self.join_team_button_present, - description="Join Team button did not become present" - ) - self.wait_for_capacity_text(remaining_members) - - @property - def team_members(self): - """Returns the number of team members in this team""" - return len(self.q(css='.page-content-secondary .team-member')) - - def click_first_profile_image(self): - """Clicks on first team member's profile image""" - self.q(css='.page-content-secondary .members-info .team-member').first.click() - - @property - def first_member_username(self): - """Returns the username of team member""" - return self.q(css='.page-content-secondary .tooltip-custom').text[0] - - def click_join_team_button(self, total_members=1): - """ Click on Join Team button""" - self.q(css='.join-team .action-primary').first.click() - self.wait_for( - lambda: not self.join_team_button_present, - description="Join Team button did not go away" - ) - self.wait_for_capacity_text(total_members) - - def wait_for_capacity_text(self, num_members, max_size=10): - """ Wait for the team capacity text to be correct. """ - self.wait_for( - lambda: self.team_capacity_text == self.format_capacity_text(num_members, max_size), - description="Team capacity text is not correct" - ) - - def format_capacity_text(self, num_members, max_size): - """ Helper method to format the expected team capacity text. """ - return u'{num_members} / {max_size} {members_text}'.format( - num_members=num_members, - max_size=max_size, - members_text='Member' if num_members == max_size else 'Members' - ) - - @property - def join_team_message(self): - """ Returns join team message """ - self.wait_for_ajax() - return self.q(css='.join-team .join-team-message').text[0] - - @property - def join_team_button_present(self): - """ Returns True if Join Team button is present else False """ - return self.q(css='.join-team .action-primary').present - - @property - def join_team_message_present(self): - """ Returns True if Join Team message is present else False """ - return self.q(css='.join-team .join-team-message').present - - @property - def new_post_button_present(self): - """ Returns True if New Post button is present else False """ - return self.q(css='.discussion-module .new-post-btn').visible - - @property - def edit_team_button_present(self): - """ Returns True if Edit Team button is present else False """ - return self.q(css='.form-actions .action-edit-team').present - - def click_edit_team_button(self): - """ Click on Edit Team button""" - self.q(css='.form-actions .action-edit-team').first.click() diff --git a/common/test/acceptance/pages/lms/track_selection.py b/common/test/acceptance/pages/lms/track_selection.py deleted file mode 100644 index bd5e6773f5..0000000000 --- a/common/test/acceptance/pages/lms/track_selection.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Track selection page""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.lms import BASE_URL -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow - - -class TrackSelectionPage(PageObject): - """Interact with the track selection page. - - This page can be accessed at `/course_modes/choose/{course_id}/`. - """ - def __init__(self, browser, course_id): - """Initialize the page. - - Arguments: - browser (Browser): The browser instance. - course_id (unicode): The course in which the user is enrolling. - """ - super(TrackSelectionPage, self).__init__(browser) - self._course_id = course_id - - @property - def url(self): - """Return the URL corresponding to the track selection page.""" - url = "{base}/course_modes/choose/{course_id}/".format( - base=BASE_URL, - course_id=self._course_id - ) - - return url - - def is_browser_on_page(self): - """Check if the track selection page has loaded.""" - return self.q(css=".wrapper-register-choose").is_present() - - def enroll(self, mode="audit"): - """Interact with one of the enrollment buttons on the page. - - Keyword Arguments: - mode (str): Can be "audit" or "verified" - - Raises: - ValueError - """ - if mode == "verified": - # Check the first contribution option, then click the enroll button - self.q(css=".contribution-option > input").first.click() - self.q(css="button[name='verified_mode']").click() - return PaymentAndVerificationFlow(self.browser, self._course_id).wait_for_page() - - elif mode == "audit": - self.q(css="input[name='audit_mode']").click() - return CourseHomePage(self.browser, self._course_id).wait_for_page() - - else: - raise ValueError("Mode must be either 'audit' or 'verified'.") diff --git a/common/test/acceptance/pages/lms/video/video.py b/common/test/acceptance/pages/lms/video/video.py index 4806b72c05..34c7d04e06 100644 --- a/common/test/acceptance/pages/lms/video/video.py +++ b/common/test/acceptance/pages/lms/video/video.py @@ -3,16 +3,11 @@ Video player in the courseware. """ -import json import logging -import time -import requests from bok_choy.javascript import js_defined, wait_for_js from bok_choy.page_object import PageObject from bok_choy.promise import EmptyPromise, Promise -from selenium.webdriver.common.action_chains import ActionChains -from six.moves import zip log = logging.getLogger('VideoPage') @@ -94,18 +89,6 @@ class VideoPage(PageObject): video_selector = '{0}'.format(CSS_CLASS_NAMES['video_container']) self.wait_for_element_presence(video_selector, 'Video is initialized') - def scroll_to_button(self, button_name, index=0): - """ - Scroll to a button specified by `button_name` - - Arguments: - button_name (str): button name - index (int): query index - - """ - element = self.q(css=VIDEO_BUTTONS[button_name])[index] - self.browser.execute_script("arguments[0].scrollIntoView();", element) - @wait_for_js def wait_for_video_player_render(self, autoplay=False): """ @@ -139,34 +122,6 @@ class VideoPage(PageObject): self.wait_for_ajax() - @wait_for_js - def wait_for_video_bumper_render(self): - """ - Wait until Poster, Video Pre-Roll and main Video Player are Rendered Completely. - """ - self.wait_for_video_class() - self.wait_for_element_presence(CSS_CLASS_NAMES['video_init'], 'Video Player Initialized') - self.wait_for_element_presence(CSS_CLASS_NAMES['video_time'], 'Video Player Initialized') - - video_player_buttons = ['do_not_show_again', 'skip_bumper', 'volume'] - for button in video_player_buttons: - self.wait_for_element_visibility(VIDEO_BUTTONS[button], u'{} button is visible'.format(button)) - - @property - def is_poster_shown(self): - """ - Check whether a poster is show. - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['poster']) - return self.q(css=selector).visible - - def click_on_poster(self): - """ - Click on the video poster. - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['poster']) - self.q(css=selector).click() - def get_video_vertical_selector(self, video_display_name=None): """ Get selector for a video vertical with display name specified by `video_display_name`. @@ -215,96 +170,6 @@ class VideoPage(PageObject): """ self.current_video_display_name = video_display_name - def is_video_rendered(self, mode): - """ - Check that if video is rendered in `mode`. - - Arguments: - mode (str): Video mode, one of `html5`, `youtube`, `hls`. - - Returns: - bool: Tells if video is rendered in `mode`. - - """ - selector = self.get_element_selector(VIDEO_MODES[mode]) - - def _is_element_present(): - """ - Check if a web element is present in DOM. - - Returns: - tuple: (is_satisfied, result)`, where `is_satisfied` is a boolean indicating whether the promise was - satisfied, and `result` is a value to return from the fulfilled `Promise`. - - """ - is_present = self.q(css=selector).present - # There is no way to get actual HLS video URL. Becuase in hls video - # src attribute is not set to original url. https://github.com/video-dev/hls.js/issues/1052 - # http://www.streambox.fr/playlists/x36xhzz/x36xhzz.m3u8 becomes - # "blob:https://studio-hlsvideo.sandbox.edx.org/0e2e72e0-904e-d946-9ce0-06c542894cda" - if mode == 'hls': - href_src = self.q(css=selector).attrs('src')[0] - is_present = href_src.startswith('blob:') or href_src.startswith('mediasource:') - return is_present, is_present - - return Promise(_is_element_present, u'Video Rendering Failed in {0} mode.'.format(mode)).fulfill() - - @property - def video_download_url(self): - """ - Return video download url or None - """ - browser_query = self.q(css='.wrapper-download-video .btn-link.video-sources') - return browser_query.attrs('href')[0] if browser_query.visible else None - - @property - def is_autoplay_enabled(self): - """ - Extract autoplay value of `data-metadata` attribute to check video autoplay is enabled or disabled. - - Returns: - bool: Tells if autoplay enabled/disabled. - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['video_container']) - auto_play = json.loads(self.q(css=selector).attrs('data-metadata')[0])['autoplay'] - return auto_play - - @property - def is_error_message_shown(self): - """ - Checks if video player error message shown. - - Returns: - bool: Tells about error message visibility. - - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['error_message']) - return self.q(css=selector).visible - - @property - def is_spinner_shown(self): - """ - Checks if video spinner shown. - - Returns: - bool: Tells about spinner visibility. - - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['video_spinner']) - return self.q(css=selector).visible - - @property - def error_message_text(self): - """ - Extract video player error message text. - - Returns: - str: Error message text. - - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['error_message']) - return self.q(css=selector).text[0] - def is_button_shown(self, button_id): """ Check if a video button specified by `button_id` is visible. @@ -325,24 +190,6 @@ class VideoPage(PageObject): """ self._captions_visibility(True) - def hide_captions(self): - """ - Make Captions Invisible. - """ - self._captions_visibility(False) - - def show_closed_captions(self): - """ - Make closed captions visible. - """ - self._closed_captions_visibility(True) - - def hide_closed_captions(self): - """ - Make closed captions invisible. - """ - self._closed_captions_visibility(False) - def is_captions_visible(self): """ Get current visibility sate of captions. @@ -355,18 +202,6 @@ class VideoPage(PageObject): caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['captions']) return self.q(css=caption_state_selector).visible - def is_closed_captions_visible(self): - """ - Get current visibility sate of closed captions. - - Returns: - bool: True means captions are visible, False means captions are not visible - - """ - self.wait_for_ajax() - closed_caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions']) - return self.q(css=closed_caption_state_selector).visible - @wait_for_js def _captions_visibility(self, captions_new_state): """ @@ -390,577 +225,3 @@ class VideoPage(PageObject): # Verify that captions state is toggled/changed EmptyPromise(lambda: self.is_captions_visible() == captions_new_state, u"Transcripts are {state}".format(state=state)).fulfill() - - @wait_for_js - def _closed_captions_visibility(self, closed_captions_new_state): - """ - Set the video closed captioning visibility state. - - Arguments: - closed_captions_new_state (bool): True means show closed captioning - """ - states = {True: 'shown', False: 'hidden'} - state = states[closed_captions_new_state] - - self.click_player_button('cc_button') - - # Make sure that the captions are visible - EmptyPromise(lambda: self.is_closed_captions_visible() == closed_captions_new_state, - u"Closed captions are {state}".format(state=state)).fulfill() - - @property - def captions_text(self): - """ - Extract captions text. - - Returns: - str: Captions Text. - - """ - self.wait_for_captions() - - captions_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_text']) - subs = self.q(css=captions_selector).html - - return ' '.join(subs) - - @property - def closed_captions_text(self): - """ - Extract closed captioning text. - - Returns: - str: closed captions Text. - - """ - self.wait_for_closed_captions() - - closed_captions_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions']) - subs = self.q(css=closed_captions_selector).html - - return ' '.join(subs) - - def click_transcript_line(self, line_no): - """ - Clicks a line in the transcript updating the current caption. - - Arguments: - line_no (int): line number to be clicked - """ - - self.wait_for_captions() - captions_selector = self.q(css=CSS_CLASS_NAMES['captions_text_getter'].format(line_no)) - captions_selector.click() - - @property - def active_caption_text(self): - """ - Return active caption text. - """ - return self.q(css=CSS_CLASS_NAMES['active_caption_text']).text[0] - - @property - def speed(self): - """ - Get current video speed value. - - Return: - str: speed value - - """ - speed_selector = self.get_element_selector(CSS_CLASS_NAMES['video_speed']) - return self.q(css=speed_selector).text[0] - - @speed.setter - def speed(self, speed): - """ - Change the video play speed. - - Arguments: - speed (str): Video speed value - - """ - # mouse over to video speed button - self.scroll_to_button('speed') - speed_menu_selector = self.get_element_selector(VIDEO_BUTTONS['speed']) - element_to_hover_over = self.q(css=speed_menu_selector).results[0] - hover = ActionChains(self.browser).move_to_element(element_to_hover_over) - hover.perform() - - speed_selector = self.get_element_selector(u'li[data-speed="{speed}"] .control'.format(speed=speed)) - self.q(css=speed_selector).first.click() - # Click triggers an ajax event - self.wait_for_ajax() - - def verify_speed_changed(self, expected_speed): - """ - Wait for the video to change its speed to the expected value. If it does not change, - the wait call will fail the test. - """ - self.wait_for(lambda: self.speed == expected_speed, "Video speed changed") - - def click_player_button(self, button): - """ - Click on `button`. - - Arguments: - button (str): key in VIDEO_BUTTONS dictionary, its value will give us the css selector for `button` - - """ - button_selector = self.get_element_selector(VIDEO_BUTTONS[button]) - - # If we are going to click pause button, Ensure that player is not in buffering state - if button == 'pause': - self.wait_for(lambda: self.state != 'buffering', 'Player is Ready for Pause') - - self.q(css=button_selector).first.click() - - self.wait_for_ajax() - - def _get_element_dimensions(self, selector): - """ - Gets the width and height of element specified by `selector` - - Arguments: - selector (str): css selector of a web element - - Returns: - dict: Dimensions of a web element. - - """ - element = self.q(css=selector).results[0] - return element.size - - @property - def _dimensions(self): - """ - Gets the video player dimensions. - - Returns: - tuple: Dimensions - - """ - iframe_selector = self.get_element_selector('.video-player iframe,') - video_selector = self.get_element_selector(' .video-player video') - video = self._get_element_dimensions(iframe_selector + video_selector) - wrapper = self._get_element_dimensions(self.get_element_selector('.tc-wrapper')) - controls = self._get_element_dimensions(self.get_element_selector('.video-controls')) - progress_slider = self._get_element_dimensions( - self.get_element_selector('.video-controls > .slider')) - - expected = dict(wrapper) - expected['height'] -= controls['height'] + 0.5 * progress_slider['height'] - - return video, expected - - def is_aligned(self, is_transcript_visible): - """ - Check if video is aligned properly. - - Arguments: - is_transcript_visible (bool): Transcript is visible or not. - - Returns: - bool: Alignment result. - - """ - # Width of the video container in css equal 75% of window if transcript enabled - wrapper_width = 75 if is_transcript_visible else 100 - initial = self.browser.get_window_size() - - self.browser.set_window_size(300, 600) - - # Wait for browser to resize completely - # Currently there is no other way to wait instead of explicit wait - time.sleep(0.2) - - real, expected = self._dimensions - - width = round(100 * real['width'] / expected['width']) == wrapper_width - - self.browser.set_window_size(600, 300) - - # Wait for browser to resize completely - # Currently there is no other way to wait instead of explicit wait - time.sleep(0.2) - - real, expected = self._dimensions - - height = abs(expected['height'] - real['height']) <= 5 - - # Restore initial window size - self.browser.set_window_size( - initial['width'], initial['height'] - ) - - return all([width, height]) - - def _get_transcript(self, url): - """ - Download Transcript from `url` - - """ - kwargs = dict() - - session_id = [{i['name']: i['value']} for i in self.browser.get_cookies() if i['name'] == u'sessionid'] - if session_id: - kwargs.update({ - 'cookies': session_id[0] - }) - - response = requests.get(url, **kwargs) - return response.status_code < 400, response.headers, response.content - - def get_cookie(self, cookie_name): - """ - Searches for and returns `cookie_name` - """ - return self.browser.get_cookie(cookie_name) - - def downloaded_transcript_contains_text(self, transcript_format, text_to_search): - """ - Download the transcript in format `transcript_format` and check that it contains the text `text_to_search` - - Arguments: - transcript_format (str): Transcript file format `srt` or `txt` - text_to_search (str): Text to search in Transcript. - - Returns: - bool: Transcript download result. - - """ - transcript_selector = self.get_element_selector(VIDEO_MENUS['transcript-format'][transcript_format]) - - # check if we have a transcript with correct format - if '.' + transcript_format not in self.q(css=transcript_selector).text[0]: - return False - - formats = { - 'srt': 'application/x-subrip', - 'txt': 'text/plain', - } - - link = self.q(css=transcript_selector) - url = link.attrs('href')[0] - link.click() - - result, headers, content = self._get_transcript(url) - - if result is False: - return False - - if text_to_search not in content.decode('utf-8'): - return False - - return True - - def current_language(self): - """ - Get current selected video transcript language. - """ - selector = self.get_element_selector(VIDEO_MENUS["language"] + ' li.is-active') - return self.q(css=selector).first.attrs('data-lang-code')[0] - - def select_language(self, code): - """ - Select captions for language `code`. - - Arguments: - code (str): two character language code like `en`, `zh`. - - """ - self.wait_for_ajax() - - # TODO remove this sleep and wait for the right thing to finish rendering - time.sleep(1) - - # mouse over to transcript button - cc_button_selector = self.get_element_selector(VIDEO_BUTTONS["transcript_button"]) - element_to_hover_over = self.q(css=cc_button_selector).results[0] - ActionChains(self.browser).move_to_element(element_to_hover_over).perform() - - language_selector = VIDEO_MENUS["language"] + u' li[data-lang-code="{code}"]'.format(code=code) - language_selector = self.get_element_selector(language_selector) - self.wait_for_element_visibility(language_selector, 'language menu is visible') - hover_target = self.q(css=language_selector).results[0] - ActionChains(self.browser).move_to_element(hover_target).perform() - self.q(css=language_selector).first.click() - - # Sometimes language is not clicked correctly. So, if the current language code - # differs form the expected, we try to change it again. - if self.current_language() != code: - self.select_language(code) - - if 'is-active' != self.q(css=language_selector).attrs('class')[0]: - return False - - active_lang_selector = self.get_element_selector(VIDEO_MENUS["language"] + ' li.is-active') - if len(self.q(css=active_lang_selector).results) != 1: - return False - - # Make sure that all ajax requests that affects the display of captions are finished. - # For example, request to get new translation etc. - self.wait_for_ajax() - - captions_selector = self.get_element_selector(CSS_CLASS_NAMES['captions']) - EmptyPromise(lambda: self.q(css=captions_selector).visible, 'Subtitles Visible').fulfill() - - self.wait_for_captions() - - return True - - def is_menu_present(self, menu_name): - """ - Check if menu `menu_name` exists. - - Arguments: - menu_name (str): Menu key from VIDEO_MENUS. - - Returns: - bool: Menu existence result - - """ - selector = self.get_element_selector(VIDEO_MENUS[menu_name]) - return self.q(css=selector).present - - @property - def sources(self): - """ - Extract all video source urls on current page. - - Returns: - list: Video Source URLs. - - """ - sources_selector = self.get_element_selector(CSS_CLASS_NAMES['video_sources']) - return self.q(css=sources_selector).map(lambda el: el.get_attribute('src').split('?')[0]).results - - @property - def caption_languages(self): - """ - Get caption languages available for a video. - - Returns: - dict: Language Codes('en', 'zh' etc) as keys and Language Names as Values('English', 'Chinese' etc) - - """ - languages_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_lang_list']) - language_codes = self.q(css=languages_selector).attrs('data-lang-code') - language_names = self.q(css=languages_selector).attrs('textContent') - - return dict(list(zip(language_codes, language_names))) - - @property - def position(self): - """ - Gets current video slider position. - - Returns: - str: current seek position in format min:sec. - - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['video_time']) - current_seek_position = self.q(css=selector).text[0] - return current_seek_position.split('/')[0].strip() - - @property - def seconds(self): - """ - Extract seconds part from current video slider position. - - Returns: - str - - """ - return int(self.position.split(':')[1]) - - @property - def state(self): - """ - Extract the current state (play, pause etc) of video. - - Returns: - str: current video state - - """ - state_selector = self.get_element_selector(CSS_CLASS_NAMES['video_container']) - current_state = self.q(css=state_selector).attrs('class')[0] - - # For troubleshooting purposes show what the current state is. - # The debug statements will only be displayed in the event of a failure. - logging.debug(u"Current state of '{}' element is '{}'".format(state_selector, current_state)) - - # See the JS video player's onStateChange function - if 'is-playing' in current_state: - return 'playing' - elif 'is-paused' in current_state: - return 'pause' - elif 'is-buffered' in current_state: - return 'buffering' - elif 'is-ended' in current_state: - return 'finished' - - def _wait_for(self, check_func, desc, result=False, timeout=200, try_interval=0.2): - """ - Calls the method provided as an argument until the Promise satisfied or BrokenPromise - - Arguments: - check_func (callable): Function that accepts no arguments and returns a boolean indicating whether the promise is fulfilled. - desc (str): Description of the Promise, used in log messages. - result (bool): Indicates whether we need a results from Promise or not - timeout (float): Maximum number of seconds to wait for the Promise to be satisfied before timing out. - - """ - if result: - return Promise(check_func, desc, timeout=timeout, try_interval=try_interval).fulfill() - else: - return EmptyPromise(check_func, desc, timeout=timeout, try_interval=try_interval).fulfill() - - def wait_for_state(self, state): - """ - Wait until `state` occurs. - - Arguments: - state (str): State we wait for. - - """ - self._wait_for( - lambda: self.state == state, - u'State is {state}'.format(state=state) - ) - - def seek(self, seek_value): - """ - Seek the video to position specified by `seek_value`. - - Arguments: - seek_value (str): seek value - - """ - seek_time = _parse_time_str(seek_value) - seek_selector = self.get_element_selector(' .video') - js_code = u"$('{seek_selector}').data('video-player-state').videoPlayer.onSlideSeek({{time: {seek_time}}})".format( - seek_selector=seek_selector, seek_time=seek_time) - self.browser.execute_script(js_code) - - # after seek, player goes into `is-buffered` state. we need to get - # out of this state before doing any further operation/action. - def _is_buffering_completed(): - """ - Check if buffering completed - """ - return self.state != 'buffering' - - self._wait_for(_is_buffering_completed, 'Buffering completed after Seek.') - self.wait_for_position(seek_value) - - def reload_page(self): - """ - Reload/Refresh the current video page. - """ - self.browser.refresh() - self.wait_for_video_player_render() - - @property - def duration(self): - """ - Extract video duration. - - Returns: - str: duration in format min:sec - - """ - selector = self.get_element_selector(CSS_CLASS_NAMES['video_time']) - - # The full time has the form "0:32 / 3:14" elapsed/duration - all_times = self.q(css=selector).text[0] - - duration_str = all_times.split('/')[1] - - return duration_str.strip() - - def wait_for_position(self, position): - """ - Wait until current will be equal to `position`. - - Arguments: - position (str): position we wait for. - - """ - self._wait_for( - lambda: self.position == position, - u'Position is {position}'.format(position=position) - ) - - @property - def is_quality_button_visible(self): - """ - Get the visibility state of quality button - - Returns: - bool: visibility status - - """ - selector = self.get_element_selector(VIDEO_BUTTONS['quality']) - return self.q(css=selector).visible - - @property - def is_quality_button_active(self): - """ - Check if quality button is active or not. - - Returns: - bool: active status - - """ - selector = self.get_element_selector(VIDEO_BUTTONS['quality']) - - classes = self.q(css=selector).attrs('class')[0].split() - return 'active' in classes - - @property - def is_transcript_skip_visible(self): - """ - Checks if the skip-to containers in transcripts are present and visible. - - Returns: - bool - """ - selector = self.get_element_selector(VIDEO_MENUS['transcript-skip']) - return self.q(css=selector).visible - - def wait_for_captions(self): - """ - Wait until captions rendered completely. - """ - captions_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_rendered']) - self.wait_for_element_visibility(captions_rendered_selector, 'Captions Rendered') - - def wait_for_closed_captions(self): - """ - Wait until closed captions are rendered completely. - """ - cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions']) - self.wait_for_element_visibility(cc_rendered_selector, 'Closed captions rendered') - - def wait_for_closed_captions_to_be_hidden(self): - """ - Waits for the closed captions to be turned off completely. - """ - cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions']) - self.wait_for_element_invisibility(cc_rendered_selector, 'Closed captions hidden') - - -def _parse_time_str(time_str): - """ - Parse a string of the form 1:23 into seconds (int). - - Arguments: - time_str (str): seek value - - Returns: - int: seek value in seconds - - """ - time_obj = time.strptime(time_str, '%M:%S') - return time_obj.tm_min * 60 + time_obj.tm_sec diff --git a/common/test/acceptance/pages/studio/asset_index.py b/common/test/acceptance/pages/studio/asset_index.py deleted file mode 100644 index d8f465aaa0..0000000000 --- a/common/test/acceptance/pages/studio/asset_index.py +++ /dev/null @@ -1,378 +0,0 @@ -""" -The Files and Uploads page for a course in Studio -""" - - -import os - -import six -from bok_choy.javascript import wait_for_js -from bok_choy.promise import EmptyPromise -from opaque_keys.edx.locator import CourseLocator -from path import Path -from six.moves import zip - -from common.test.acceptance.pages.common.utils import sync_on_notification -from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.course_page import CoursePage - -# file path found from CourseFixture logic -UPLOAD_SUFFIX = '/data/uploads/studio-uploads/' -UPLOAD_FILE_DIR = Path(__file__).abspath().dirname().dirname().dirname().dirname() + UPLOAD_SUFFIX - - -class AssetIndexPageStudioFrontend(CoursePage): - """The Files and Uploads page for a course in Studio""" - - PAGINATION_PAGE_ELEMENT = ".pagination .page-item" - TABLE_SORT_BUTTONS = 'th.sortable button.btn-header' - TYPE_FILTER_ELEMENT = 'div[data-identifier="asset-filters"] .form-group' - URL_PATH = "assets" - - @property - def url(self): - """Construct a URL to the page within the course.""" - # TODO - is there a better way to make this agnostic to the underlying default module store? - default_store = os.environ.get('DEFAULT_STORE', 'draft') - course_key = CourseLocator( - self.course_info['course_org'], - self.course_info['course_num'], - self.course_info['course_run'], - deprecated=(default_store == 'draft') - ) - url = "/".join([BASE_URL, self.URL_PATH, six.moves.urllib.parse.quote_plus(six.text_type(course_key))]) - return url if url[-1] == '/' else url + '/' - - @wait_for_js - def is_browser_on_page(self): - return all([ - self.q(css='body.view-uploads').present, - self.q(css='.page-header').present, - self.q(css='#root').present, - not self.q(css='div.ui-loading').visible, - ]) - - @wait_for_js - def is_studio_frontend_container_on_page(self): - """Checks that the studio-frontend container has been loaded.""" - return self.q(css='.SFE').present - - @wait_for_js - def is_table_element_on_page(self): - """Checks that table is on the page.""" - return self.q(css='table.table-responsive').present - - @wait_for_js - def is_upload_element_on_page(self): - """Checks that the dropzone area is on the page.""" - return self.q(css='.drop-zone').present - - @wait_for_js - def is_filter_element_on_page(self): - """Checks that type filter heading and checkboxes are on the page.""" - return all([ - self.q(css='.filter-heading').is_present, - self.q(css=self.TYPE_FILTER_ELEMENT).present, - ]) - - @wait_for_js - def is_pagination_element_on_page(self): - """Checks that pagination is on the page.""" - return self.q(css='.pagination').present - - @wait_for_js - def is_search_element_on_page(self): - """Checks that search bar is on the page.""" - return self.q(css="[name='search']").present - - @wait_for_js - def is_status_alert_element_on_page(self): - """Checks that status alert is hidden on page.""" - return all([ - self.q(css='.alert').present, - not self.q(css='.alert').visible, - ]) - - @wait_for_js - def are_no_results_headings_on_page(self): - """Checks that no results page text is on page.""" - return self.q(css='.SFE-wrapper h3').filter(lambda el: el.text == '0 files found').present - - @wait_for_js - def is_no_results_clear_filter_button_on_page(self): - """Checks that no results clear filter button is on page.""" - return self.q(css='.SFE-wrapper button.btn').filter( - lambda el: el.text == 'Clear all filters' - ).present - - @property - @wait_for_js - def asset_files_names(self): - """ - Get the names of uploaded files. - Returns: - list: Names of files on current page. - """ - return self.q(css='span[data-identifier="asset-file-name"]').text - - @property - @wait_for_js - def asset_files_types(self): - """ - Get the file types of uploaded files. - Returns: - list: File types of files on current page. - """ - return self.q(css='span[data-identifier="asset-content-type"]').text - - @property - @wait_for_js - def number_of_asset_files(self): - """ - Returns the number of files on the current page. - """ - return len(self.q(css='span[data-identifier="asset-file-name"]').execute()) - - @property - @wait_for_js - def number_of_filters(self): - return len(self.q(css='.form-check').execute()) - - @property - @wait_for_js - def number_of_sortable_buttons_in_table_heading(self): - return len(self.q(css=self.TABLE_SORT_BUTTONS).execute()) - - @property - @wait_for_js - def asset_delete_buttons(self): - """Return a list of WebElements for deleting the assets""" - css = 'button[data-identifier="asset-delete-button"]' - return self.q(css=css).execute() - - @wait_for_js - def asset_lock_buttons(self, locked_only=True): - """ - Return a list of WebElements of the lock buttons for assets - or an empty list if there are none. - """ - css = 'button[data-identifier="asset-lock-button"]' - if locked_only: - css = '{}.{}'.format(css, 'fa-lock') - return self.q(css=css).execute() - - @wait_for_js - def select_type_filter(self, filter_number): - """ - Selects Images Type filter checkbox which filters the results. - Returns False if no filter. - """ - self.wait_for_ajax() - if self.is_filter_element_on_page(): - self.q(css=self.TYPE_FILTER_ELEMENT + ' .form-check .form-check-input').nth(filter_number).click() - self.wait_for_ajax() - return True - return False - - @wait_for_js - def click_clear_filters_button(self): - """ - Clicks 'Clear all filters' button. - Returns False if no 'Clear all filters' button. - """ - self.wait_for_ajax() - if self.is_no_results_clear_filter_button_on_page(): - self.q(css='.SFE-wrapper button.btn').filter( - lambda el: el.text == 'Clear all filters' - ).click() - self.wait_for_ajax() - return True - return False - - @wait_for_js - def set_asset_lock(self, index=0): - """ - Set the state of the asset in the row specified by index - to locked or unlocked by clicking the button. - Note: this will raise an IndexError if the row does not exist. - """ - lock_button = self.q(css=".table-responsive tbody tr td:nth-child(7) button").execute()[index] - lock_button.click() - # Click initiates an ajax call, waiting for it to complete - self.wait_for_ajax() - sync_on_notification(self) - - @wait_for_js - def confirm_asset_deletion(self): - """ Click to confirm deletion and sync on the notification.""" - confirmation_title_selector = '.modal' - self.q(css=confirmation_title_selector + ' button[data-identifier="asset-confirm-delete-button"]').click() - # Click initiates an ajax call, waiting for it to complete - self.wait_for_ajax() - sync_on_notification(self) - - @wait_for_js - def delete_first_asset(self): - """ Deletes file then clicks delete on confirmation.""" - self.q(css='.fa-trash').first.click() - self.confirm_asset_deletion() - - @wait_for_js - def delete_asset_named(self, name): - """Delete the asset with the specified name.""" - names = self.asset_files_names - if name not in names: - raise LookupError(u'Asset with filename {} not found.'.format(name)) - delete_buttons = self.asset_delete_buttons - assets = dict(list(zip(names, delete_buttons))) - # Now click the link in that row - assets.get(name).click() - self.confirm_asset_deletion() - - @wait_for_js - def delete_all_assets(self): - """Delete all uploaded assets.""" - while self.number_of_asset_files: - self.delete_first_asset() - - self.wait_for_ajax() - self.wait_for_page() - - @wait_for_js - def upload_new_files(self, file_names): - """ - Upload file(s). - - Arguments: - file_names (list): file name(s) we want to upload. - """ - file_input_css = 'input[type=file]' - - for file_name in file_names: - # Make file input field visible. - self.browser.execute_script('$("{}").css("display","block");'.format(file_input_css)) - self.wait_for_element_visibility(file_input_css, "Input is visible") - # Send file to upload - self.q(css=file_input_css).results[0].send_keys( - UPLOAD_FILE_DIR + file_name) - self.q(css=file_input_css).results[0].clear() - # Wait for status alert and close - self.wait_for_element_visibility( - '.alert', 'Upload status alert is visible.') - self.q(css='.close').first.click() - - self.wait_for_ajax() - self.wait_for_files_upload(len(file_names)) - - @wait_for_js - def wait_for_files_upload(self, number): - """ - Wait for file(s) to upload. - - Arguments: - number (int): number of uploaded files. - """ - return EmptyPromise( - lambda: self.number_of_asset_files == number, - "Files finished uploading" - ).fulfill() - - @property - @wait_for_js - def is_previous_button_enabled(self): - return 'disabled' not in self.q(css=self.PAGINATION_PAGE_ELEMENT).first.attrs('class')[0] - - @property - @wait_for_js - def is_next_button_enabled(self): - return 'disabled' not in self.q(css=self.PAGINATION_PAGE_ELEMENT).nth( - self.number_of_pagination_buttons - 1).attrs('class')[0] - - @property - @wait_for_js - def is_previous_button_on_page(self): - """Note: the two conditions cover when the button is and is not disabled.""" - return 'previous' in self.q(css=self.PAGINATION_PAGE_ELEMENT + ' .previous').text - - @property - @wait_for_js - def is_next_button_on_page(self): - """Note: the two conditions cover when the button is and is not disabled.""" - return 'next' in self.q(css=self.PAGINATION_PAGE_ELEMENT + ' .next').text - - @wait_for_js - def click_pagination_page_button(self, index): - """ - Click pagination page button. - Return False if no pagination page button at specified index. - """ - self.wait_for_ajax() - if index <= self.number_of_pagination_buttons: - self.q(css=self.PAGINATION_PAGE_ELEMENT + ' .page-link').nth(index)[0].click() - self.wait_for_ajax() - return True - return False - - @wait_for_js - def click_pagination_next_button(self): - """ - Click pagination next button. - Return False if next button disabled. - """ - self.wait_for_ajax() - if self.is_next_button_enabled: - self.q(css=self.PAGINATION_PAGE_ELEMENT + ' .next.page-link')[0].click() - self.wait_for_ajax() - return True - return False - - @wait_for_js - def click_pagination_previous_button(self): - """ - Click pagination previous button. - Return False if previous button disabled. - """ - self.wait_for_ajax() - if self.is_previous_button_enabled: - self.q(css=self.PAGINATION_PAGE_ELEMENT + ' .previous.page-link')[0].click() - self.wait_for_ajax() - return True - return False - - @property - @wait_for_js - def number_of_pagination_buttons(self): - """Return the number of total pagination page buttons, including previous, pages, and next buttons.""" - return len(self.q(css=self.PAGINATION_PAGE_ELEMENT + ' .page-link')) - - @wait_for_js - def is_selected_page(self, index): - """ - Return true if the pagination page at the current index is selected. - Return false if the pagination page at the current index does not exist - or is not selected. - - Note: this *does* include the 'previous' and 'next' buttons - Note: 0-indexed - """ - if index < self.number_of_pagination_buttons: - return 'active' in self.q(css=self.PAGINATION_PAGE_ELEMENT).nth(index)[0].get_attribute('class') - return False - - @wait_for_js - def click_sort_button(self, button_text): - """ - Click sort button with the specified button text. - - Arguments: - button_text (string): text of the sort button to click. - """ - self.wait_for_ajax() - sort_button = self.q(css=self.TABLE_SORT_BUTTONS).filter( - lambda el: button_text in el.text - ) - - if sort_button: - sort_button.click() - return True - return False diff --git a/common/test/acceptance/pages/studio/checklists.py b/common/test/acceptance/pages/studio/checklists.py deleted file mode 100644 index bffa967a9f..0000000000 --- a/common/test/acceptance/pages/studio/checklists.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Course Checklists page. -""" - - -from common.test.acceptance.pages.studio.course_page import CoursePage - - -class CourseChecklistsPage(CoursePage): - """ - Course Checklists page. - """ - - url_path = "checklists" - - def is_browser_on_page(self): - # SFE and SFE-wrapper classes come from studio-frontend and - # wrap content provided by the studio-frontend package - return self.q(css='.SFE .SFE-wrapper').visible diff --git a/common/test/acceptance/pages/studio/course_info.py b/common/test/acceptance/pages/studio/course_info.py deleted file mode 100644 index 5b8cf2801b..0000000000 --- a/common/test/acceptance/pages/studio/course_info.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -Course Updates page. -""" - - -from common.test.acceptance.pages.common.utils import click_css, confirm_prompt -from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.pages.studio.utils import set_input_value, type_in_codemirror - - -class CourseUpdatesPage(CoursePage): - """ - Course Updates page. - """ - url_path = "course_info" - - def is_browser_on_page(self): - """ - Returns whether or not the browser on the page and has loaded the required content - """ - # Check for the presence of handouts-content, when it is present the render function has completed - # loading the updates and handout sections - return (self.q(css='.handouts-content').present and - self.q(css='article#course-update-view.course-updates').present) - - def is_course_update_list_empty(self): - """ - Checks whether or not the update contents list is empty - """ - return len(self.q(css='.update-contents')) == 0 - - def is_new_update_button_present(self): - """ - Checks for the presence of the new update post button. - """ - return self.q(css='.new-update-button').present - - def click_new_update_button(self): - """ - Clicks the new-update button. - """ - def is_update_button_enabled(): - """ - Checks if the New Update button is enabled - """ - return self.q(css='.new-update-button').attrs('disabled')[0] is None - - self.wait_for(promise_check_func=is_update_button_enabled, - description='Waiting for the New update button to be enabled.') - click_css(self, '.new-update-button', require_notification=False) - self.wait_for_element_visibility('.CodeMirror', 'Waiting for .CodeMirror') - - def submit_update(self, message): - """ - Adds update text to the new update CodeMirror form and submits that text. - - Arguments: - message (str): The message to be added and saved. - """ - type_in_codemirror(self, 0, message) - self.click_new_update_save_button() - - def set_date(self, date): - """ - Sets the updates date input to the provided value. - - Arguments: - date (str): Date string in the format DD/MM/YYYY - """ - set_input_value(self, 'input.date', date) - - def is_first_update_date(self, search_date): - """ - Checks to see if the search date is present - - Arguments: - search_date (str): e.g. 06/01/2013 would be found with June 1, 2013 - - Returns: - bool: True if the date is in the first update and False otherwise. - """ - return search_date == self.q(css='.date-display').html[0] - - def is_new_update_save_button_present(self): - """ - Checks to see if the CodeMirror Update save button is present. - """ - return self.q(css='.save-button').present - - def click_new_update_save_button(self): - """ - Clicks the CodeMirror Update save button. - """ - click_css(self, '.save-button') - - def is_edit_button_present(self): - """ - Checks to see if the edit update post buttons if present. - """ - return self.q(css='.post-preview .edit-button').present - - def click_edit_update_button(self): - """ - Clicks the edit update post button. - """ - click_css(self, '.post-preview .edit-button', require_notification=False) - self.wait_for_element_visibility('.CodeMirror', 'Waiting for .CodeMirror') - - def is_delete_update_button_present(self): - """ - Checks to see if the delete update post button is present. - """ - return self.q(css='.post-preview .delete-button').present - - def click_delete_update_button(self): - """ - Clicks the delete update post button and confirms the delete notification. - """ - click_css(self, '.post-preview .delete-button', require_notification=False) - confirm_prompt(self) - - def is_first_update_message(self, message): - """ - Looks for the message in the first course update posted. - - Arguments: - message (str): String containing the message that is to be searched for - - Returns: - bool: True if the first update is the message, false otherwise. - """ - return message == self.q(css='.update-contents').html[0] - - def first_update_contains_html(self, value): - """ - Looks to see if the html provided is contained in the first update - - Arguments: - value (str): String value that will be looked for - - Returns: - bool: True if the value is contained in the first update - """ - update = self.q(css='.update-contents').html - return value in update[0] diff --git a/common/test/acceptance/pages/studio/discussion_component_editor.py b/common/test/acceptance/pages/studio/discussion_component_editor.py deleted file mode 100644 index e775cff61c..0000000000 --- a/common/test/acceptance/pages/studio/discussion_component_editor.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Discussion component editor in studio -""" - - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.studio.xblock_editor import XBlockEditorView - - -class DiscussionComponentEditor(XBlockEditorView): - """ - Discussion Editor view in studio - """ - @property - def edit_discussion_field_values(self): - """ - Get field values of discussion edit dialogue. - Returns: - list: A list of string indicating field values - """ - return self.q(css='.field-data-control').attrs('value') - - def set_field_val(self, field_display_name, field_value): - """ - If editing, set the value of a field. - """ - selector = u'.xblock-studio_view li.field label:contains("{}") + input'.format(field_display_name) - script = "$(arguments[0]).val(arguments[1]).change();" - self.browser.execute_script(script, selector, field_value) - - def save(self): - """ - Clicks save button. - """ - click_css(self, '.save-button') diff --git a/common/test/acceptance/pages/studio/edit_tabs.py b/common/test/acceptance/pages/studio/edit_tabs.py deleted file mode 100644 index c00ae2e4ae..0000000000 --- a/common/test/acceptance/pages/studio/edit_tabs.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -Pages page for a course. -""" - - -from bok_choy.promise import EmptyPromise -from selenium.webdriver import ActionChains - -from common.test.acceptance.pages.common.utils import click_css, confirm_prompt -from common.test.acceptance.pages.studio.course_page import CoursePage - - -class PagesPage(CoursePage): - """ - Pages page for a course. - """ - - url_path = "tabs" - - def is_browser_on_page(self): - return self.q(css='body.view-static-pages').present - - def is_static_page_present(self): - """ - Checks for static tab's presence - - Returns: - bool: True if present - """ - return self.q(css='.wrapper.wrapper-component-action-header').present - - def add_static_page(self): - """ - Adds a static page - """ - total_tabs = len(self.q(css='.course-nav-list>li')) - click_css(self, '.add-pages .new-tab', require_notification=False) - self.wait_for( - lambda: len(self.q(css='.course-nav-list>li')) == total_tabs + 1, - description="Static tab is added" - ) - self.wait_for_element_visibility( - u'.tab-list :nth-child({}) .xblock-student_view'.format(total_tabs), - 'Static tab is visible' - ) - # self.wait_for_ajax() - - def delete_static_tab(self): - """ - Deletes a static page - """ - click_css(self, '.btn-default.delete-button.action-button', require_notification=False) - confirm_prompt(self) - - def click_edit_static_page(self): - """ - Clicks on edit button to open up the xblock modal - """ - self.q(css='.edit-button').first.click() - EmptyPromise( - lambda: self.q(css='.xblock-studio_view').present, - 'Wait for the Studio editor to be present' - ).fulfill() - - def drag_and_drop_first_static_page_to_last(self): - """ - Drags and drops the first the static page to the last - """ - draggable_elements = self.q(css='.component .drag-handle').results - source_element = draggable_elements[0] - target_element = self.q(css='.new-component-item').results[0] - action = ActionChains(self.browser) - action.drag_and_drop(source_element, target_element).perform() - self.wait_for_ajax() - - def drag_and_drop(self, default_tab=False): - """ - Drags and drops the first static page to the last - """ - css_selector = '.component .drag-handle' - if default_tab: - css_selector = '.drag-handle.action' - source_element = self.q(css=css_selector).results[0] - target_element = self.q(css='.new-component-item').results[0] - action = ActionChains(self.browser) - action.drag_and_drop(source_element, target_element).perform() - self.wait_for_ajax() - - @property - def static_tab_titles(self): - """ - Return titles of all static tabs - Returns: - list: list of all the titles - """ - self.wait_for_element_visibility( - '.wrapper-component-action-header .component-actions', - "Tab's edit button is visible" - ) - return self.q(css='div.xmodule_StaticTabBlock').text - - @property - def built_in_page_titles(self): - """ - Gets the default tab title - Returns: - list: list of all the titles - """ - return self.q(css='.course-nav-list.ui-sortable h3').text - - def open_settings_tab(self): - """ - Clicks settings tab - """ - self.q(css='.editor-modes .settings-button').first.click() - self.wait_for_ajax() - - def is_tab_visible(self, tab_name): - """ - Checks for the tab's visibility - Args: - tab_name(string): Name of the tab for which visibility is to be checked - Returns: - true(bool): if tab is visible - false(bool): if tab is not visible - """ - css_selector = u'[data-tab-id="{}"] .toggle-checkbox'.format(tab_name) - return True if not self.q(css=css_selector).selected else False - - def toggle_tab(self, tab_name): - """ - Toggles the visibility on tab - Args: - tab_name(string): Name of the tab to be toggled - """ - css_selector = u'[data-tab-id="{}"] .action-visible'.format(tab_name) - return self.q(css=css_selector).first.click() - - def set_field_val(self, field_display_name, field_value): - """ - Set the value of a field in editor - - Arguments: - field_display_name(str): Display name of the field for which the value is to be changed - field_value(str): New value for the field - """ - selector = u'.xblock-studio_view li.field label:contains("{}") + input'.format(field_display_name) - script = '$(arguments[0]).val(arguments[1]).change();' - self.browser.execute_script(script, selector, field_value) - - def save(self): - """ - Clicks save button. - """ - click_css(self, '.action-save') - - def refresh_and_wait_for_load(self): - """ - Refresh the page and wait for all resources to load. - """ - self.browser.refresh() - self.wait_for_page() diff --git a/common/test/acceptance/pages/studio/html_component_editor.py b/common/test/acceptance/pages/studio/html_component_editor.py deleted file mode 100644 index c0e7ca4db2..0000000000 --- a/common/test/acceptance/pages/studio/html_component_editor.py +++ /dev/null @@ -1,289 +0,0 @@ -""" -HTML component editor in studio -""" - - -from six.moves import zip - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.studio.utils import get_codemirror_value, type_in_codemirror -from common.test.acceptance.pages.studio.xblock_editor import XBlockEditorView - - -class HtmlXBlockEditorView(XBlockEditorView): - """ - Represents the rendered view of an HTML component editor. - """ - - editor_mode_css = '.edit-xblock-modal .editor-modes .editor-button' - settings_tab = '.editor-modes .settings-button' - save_settings_button = '.action-save' - - @property - def toolbar_dropdown_titles(self): - """ - Returns the titles of dropdowns present on the toolbar - """ - return self.q(css='.mce-listbox').text - - @property - def toolbar_button_titles(self): - """ - Returns the titles of the buttons present on the toolbar - Returns: - - """ - return self.q(css='.mce-ico').attrs('class') - - @property - def fonts(self): - """ - Available fonts in the font dropdown - Returns: - (list): A list of font names - """ - return self.q(css='.mce-text').text - - @property - def font_families(self): - """ - Available font families against each font - Returns: - (list): A list of font families - """ - return self.q(css='.mce-text').attrs('style') - - def open_font_dropdown(self): - """ - Clicks and waits for font dropdown to open - """ - self.q(css='#mce_2-open').first.click() - self.wait_for_element_visibility('.mce-floatpanel', 'Dropdown is Visible') - - def font_dict(self): - """ - Creates a dictionary with font labels and font families - Returns: - font_dict(dict): A dictionary of font labels as keys and font families as values - """ - font_labels = self.fonts - font_families = self.font_families - for index, font in enumerate(font_families): - font = font.replace('font-family: ', '').replace(';', '') - font_families[index] = font.split(',') - font_families[index] = [x.lstrip() for x in font_families[index]] - font_dict = dict(list(zip(font_labels, font_families))) - return font_dict - - def set_content_and_save(self, content, raw=False): - """Types content into the html component and presses Save. - - Arguments: - content (str): The content to be used. - raw (bool): If true, edits in 'raw HTML' mode. - """ - if raw: - self.set_raw_content(content) - else: - self.set_content(content) - - self.save() - - def set_content_and_cancel(self, content, raw=False): - """Types content into the html component and presses Cancel to abort. - - Arguments: - content (str): The content to be used. - raw (bool): If true, edits in 'raw HTML' mode. - """ - if raw: - self.set_raw_content(content) - else: - self.set_content(content) - - self.cancel() - - def set_content(self, content): - """Sets content in the html component, leaving the component open. - - Arguments: - content (str): The content to be used. - """ - self.q(css=self.editor_mode_css).click() - self.browser.execute_script("tinyMCE.activeEditor.setContent('%s')" % content) - - def set_raw_content(self, content): - """Types content in raw html mode, leaving the component open. - Arguments: - content (str): The content to be used. - """ - self.q(css=self.editor_mode_css).click() - self.q(css='[aria-label="Edit HTML"]').click() - self.wait_for_element_visibility('.mce-title', 'Wait for CodeMirror editor') - # Set content in the CodeMirror editor. - type_in_codemirror(self, 0, content) - - self.q(css='.mce-foot .mce-primary').click() - - def open_settings_tab(self): - """ - Clicks settings button on the modal - """ - click_css(self, self.settings_tab) - - def save_settings(self): - """ - Click save button on the modal - """ - click_css(self, self.save_settings_button) - - def open_raw_editor(self): - """ - Clicks and waits for raw editor to open - """ - self.q(css='[aria-label="Edit HTML"]').click() - self.wait_for_element_visibility('.mce-title', 'Wait for CodeMirror editor') - - def open_link_plugin(self): - """ - Opens up the link plugin on editor - """ - self.q(css='[aria-label="Insert/edit link"]').click() - self.wait_for_element_visibility('.mce-window-head', 'Window header present') - - def save_static_link(self, static_link): - """ - Adds static link inside the link plugin - """ - self.q(css='.mce-combobox .mce-textbox').fill(static_link) - self.q(css='.mce-btn.mce-primary').click() - - @property - def href(self): - """ - Gets the href from the editor - """ - return self.q(css="#tinymce>p>a").attrs('href')[0] - - @property - def editor_value(self): - """ - Returns codemirror value from raw HTMl editor - """ - return get_codemirror_value(self, 0) - - def switch_to_iframe(self): - """ - Switches to the editor iframe - """ - self.browser.switch_to_frame(self.browser.find_element_by_tag_name('iframe')) - - @property - def url_from_the_link_plugin(self): - """ - Clicks the already set link from the editor and then returns the URL from the link plugin - """ - self.open_link_plugin() - return self.browser.execute_script('return $(".mce-textbox").val();') - - def set_text_and_select(self, text): - """ - Sets and selects text from html editor - """ - script = """ - var editor = tinyMCE.activeEditor; - editor.setContent(arguments[0]); - editor.selection.select(editor.dom.select('p')[0]);""" - self.browser.execute_script(script, str(text)) - self.wait_for_ajax() - - def click_code_toolbar_button(self): - """ - Clicks on the code plugin on the toolbar - """ - self.q(css='.mce-i-none').first.click() - - def get_default_settings(self): - """ - Returns default display name and editor - """ - display_name_setting = self.q(css='.wrapper-comp-setting input[type="text"]:nth-child(2)').attrs('value')[0] - editor_setting = self.q(css='.wrapper-comp-setting .input.setting-input :nth-child(1)').text[0] - return [display_name_setting, editor_setting] - - @property - def keys(self): - """ - Gets setting keys - """ - return self.q(css='.label.setting-label[for]').text - - def set_field_val(self, field_display_name, field_value): - """ - If editing, set the value of a field. - """ - selector = u'.xblock-studio_view li.field label:contains("{}") + input'.format(field_display_name) - script = "$(arguments[0]).val(arguments[1]).change();" - self.browser.execute_script(script, selector, field_value) - - def save(self): - """ - Clicks save button. - """ - click_css(self, '.save-button') - - def save_content(self): - """ - Click save button - """ - click_css(self, '.action-save') - - def open_image_modal(self): - """ - Clicks and in insert image button - """ - click_css(self, 'div i[class="mce-ico mce-i-image"]') - - def upload_image(self, file_name): - """ - Upload image and add description and click save to upload image via TinyMCE editor. - """ - file_input_css = "[type='file']" - - # select file input element and change visibility to add file. - self.browser.execute_script('$("{}").css("display","block");'.format(file_input_css)) - self.wait_for_element_visibility(file_input_css, "Input is visible") - self.q(css=file_input_css).results[0].send_keys(file_name) - self.wait_for_element_visibility('#imageDescription', 'Upload form is visible.') - - self.q(css='#imageDescription').results[0].send_keys('test image') - click_css(self, '.modal-footer .btn-primary') - - -class HTMLEditorIframe(XBlockEditorView): - """ - Represent the iframe on HTMl editor view - """ - - def is_browser_on_page(self): - return self.q(css='#tinymce').present - - @property - def href(self): - """ - Gets the href from the editor - """ - return self.q(css="#tinymce>p>a").attrs('href')[0] - - def select_link(self): - """ - Clicks the already set link from the editor and then returns the URL from the link plugin - """ - self.q(css='#tinymce>p>a').first.click() - - def switch_to_default(self): - """ - Switches to default page - - """ - self.browser.switch_to_default_content() diff --git a/common/test/acceptance/pages/studio/import_export.py b/common/test/acceptance/pages/studio/import_export.py deleted file mode 100644 index 6b2f5f0059..0000000000 --- a/common/test/acceptance/pages/studio/import_export.py +++ /dev/null @@ -1,341 +0,0 @@ -""" -Import/Export pages. -""" - - -import os -import re -import time -from datetime import datetime - -import requests -import six -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.pages.studio.library import LibraryPage - - -class TemplateCheckMixin(object): - """ - Mixin for verifying that a template is loading the correct text. - """ - @property - def header_text(self): - """ - Get the header text of the page. - """ - # There are prefixes like 'Tools' and '>', but the text itself is not in a span. - return self.q(css='h1.page-header')[0].text.split('\n')[-1] - - -class ImportExportMixin(object): - """ - Mixin for functionality common to both the import and export pages - """ - - def is_task_list_showing(self): - """ - The task list shows a series of steps being performed during import or - export. It is normally hidden until the process begins. - - Tell us whether it's currently visible. - """ - return self.q(css='.wrapper-status').visible - - def is_timestamp_visible(self): - """ - Checks if the UTC timestamp of the last successful import/export is visible - """ - return self.q(css='.item-progresspoint-success-date').visible - - @property - def parsed_timestamp(self): - """ - Return python datetime object from the parsed timestamp tuple (date, time) - """ - timestamp = u"{0} {1}".format(*self.timestamp) - formatted_timestamp = time.strptime(timestamp, u"%m/%d/%Y %H:%M") - return datetime.fromtimestamp(time.mktime(formatted_timestamp)) - - @property - def timestamp(self): - """ - The timestamp is displayed on the page as "(MM/DD/YYYY at HH:mm)" - It parses the timestamp and returns a (date, time) tuple - """ - string = self.q(css='.item-progresspoint-success-date').text[0] - - return re.match(six.text_type(r'\(([^ ]+).+?(\d{2}:\d{2})'), string).groups() - - def wait_for_tasks(self, completed=False, fail_on=None): - """ - Wait for all of the items in the task list to be set to the correct state. - """ - if fail_on: - # Makes no sense to include this if the tasks haven't run. - completed = True - - state, desc_template = self._task_properties(completed) - - for desc, css_class in self.task_classes.items(): - desc_text = desc_template.format(desc) - # pylint: disable=cell-var-from-loop - EmptyPromise(lambda: self.q(css=u'.{}.{}'.format(css_class, state)).present, desc_text, timeout=30) - if fail_on == desc: - EmptyPromise( - lambda: self.q(css=u'.{}.is-complete.has-error'.format(css_class)).present, - u"{} checkpoint marked as failed".format(desc), - timeout=30 - ) - # The rest should never run. - state, desc_template = self._task_properties(False) - - def wait_for_timestamp_visible(self): - """ - Wait for the timestamp of the last successful import/export to be visible. - """ - EmptyPromise(self.is_timestamp_visible, 'Timestamp Visible', timeout=30).fulfill() - - @staticmethod - def _task_properties(completed): - """ - Outputs the CSS class and promise description for task states based on completion. - """ - if completed: - return 'is-complete', u"'{}' is marked complete" - else: - return 'is-not-started', u"'{}' is in not-yet-started status" - - -class ExportMixin(ImportExportMixin): - """ - Export page Mixin. - """ - - url_path = "export" - - task_classes = { - 'Preparing': 'item-progresspoint-prepare', - 'Exporting': 'item-progresspoint-export', - 'Compressing': 'item-progresspoint-compress', - 'Success': 'item-progresspoint-success' - } - - def is_browser_on_page(self): - """ - Verify this is the export page - """ - return self.q(css='body.view-export').present - - def is_click_handler_registered(self): - """ - Check if the click handler for the export button has been registered yet - """ - script = """ - var $ = require('jquery'), - buttonEvents = $._data($('a.action-primary')[0], 'events'); - return buttonEvents && buttonEvents.hasOwnProperty('click');""" - stripped_script = ''.join([line.strip() for line in script.split('\n')]) - return self.browser.execute_script(stripped_script) - - def _get_tarball(self, url): - """ - Download tarball at `url` - """ - kwargs = dict() - session_id = [{i['name']: i['value']} for i in self.browser.get_cookies() if i['name'] == u'sessionid'] - if session_id: - kwargs.update({ - 'cookies': session_id[0] - }) - - response = requests.get(url, **kwargs) - - return response.status_code == 200, response.headers - - def download_tarball(self): - """ - Downloads the course or library in tarball form. - """ - tarball_url = self.q(css='#download-exported-button')[0].get_attribute('href') - good_status, headers = self._get_tarball(tarball_url) - return good_status, headers['content-type'] == 'application/x-tgz' - - def click_export(self): - """ - Click the export button. - """ - self.q(css='a.action-export').click() - - def is_error_modal_showing(self): - """ - Indicates whether or not the error modal is showing. - """ - return self.q(css='.prompt.error').visible - - def is_export_finished(self): - """ - Checks if the 'Download Exported Course/Library' button is showing. - """ - button = self.q(css='#download-exported-button')[0] - return button.is_displayed() and button.get_attribute('href') - - def click_modal_button(self): - """ - Click the button on the modal dialog that appears when there's a problem. - """ - self.q(css='.prompt.error .action-primary').click() - - def wait_for_error_modal(self): - """ - If an import or export has an error, an error modal will be shown. - """ - EmptyPromise(self.is_error_modal_showing, 'Error Modal Displayed', timeout=30).fulfill() - - def wait_for_export(self): - """ - Wait for the export process to finish. - """ - EmptyPromise(self.is_export_finished, 'Export Finished', timeout=30).fulfill() - - def wait_for_export_click_handler(self): - """ - Wait for the export button click handler to be registered - """ - EmptyPromise(self.is_click_handler_registered, 'Export Button Click Handler Registered', timeout=30).fulfill() - - -class LibraryLoader(object): - """ - URL loading mixing for Library import/export - """ - @property - def url(self): - """ - This pattern isn't followed universally by library URLs, - but is used for import/export. - """ - # pylint: disable=no-member - return "/".join([BASE_URL, self.url_path, six.text_type(self.locator)]) - - -class ExportCoursePage(ExportMixin, TemplateCheckMixin, CoursePage): - """ - Export page for Courses - """ - - -class ExportLibraryPage(ExportMixin, TemplateCheckMixin, LibraryLoader, LibraryPage): - """ - Export page for Libraries - """ - - -class ImportMixin(ImportExportMixin): - """ - Import page mixin - """ - - url_path = "import" - - task_classes = { - 'Uploading': 'item-progresspoint-upload', - 'Unpacking': 'item-progresspoint-unpack', - 'Verifying': 'item-progresspoint-verify', - 'Updating': 'item-progresspoint-import', - 'Success': 'item-progresspoint-success' - } - - def is_browser_on_page(self): - """ - Verify this is the export page - """ - return self.q(css='.choose-file-button').present - - @staticmethod - def file_path(filename): - """ - Construct file path to be uploaded from the data upload folder. - - Arguments: - filename (str): asset filename - - """ - # Should grab common point between this page module and the data folder. - return os.sep.join(__file__.split(os.sep)[:-4]) + '/data/imports/' + filename - - def _wait_for_button(self): - """ - Wait for the upload button to appear. - """ - return EmptyPromise( - lambda: self.q(css='#replace-courselike-button')[0], - "Upload button appears", - timeout=30 - ).fulfill() - - def upload_tarball(self, tarball_filename): - """ - Upload a tarball to be imported. - """ - asset_file_path = self.file_path(tarball_filename) - # Make the upload elements visible to the WebDriver. - self.browser.execute_script('$(".file-name-block").show();$(".file-input").show()') - # Upload the file. - self.q(css='input[type="file"]')[0].send_keys(asset_file_path) - # Upload the same file again. Reason behind this is to decrease the - # probability or fraction of times the failure occur. Please be - # noted this doesn't eradicate the root cause of the error, it - # just decreases to failure rate to minimal. - # Jira ticket reference: TNL-4191. - self.q(css='input[type="file"]')[0].send_keys(asset_file_path) - # Some of the tests need these lines to pass so don't remove them. - self._wait_for_button() - click_css(self, '.submit-button', require_notification=True) - - def is_upload_finished(self): - """ - Checks if the 'view updated' button is showing. - """ - return self.q(css='#view-updated-button').visible - - def wait_for_upload(self): - """ - Wait for the upload to be confirmed. - """ - EmptyPromise(self.is_upload_finished, 'Upload Finished', timeout=30).fulfill() - - def is_filename_error_showing(self): - """ - An should be shown if the user tries to upload the wrong kind of file. - - Tell us whether it's currently being shown. - """ - return self.q(css='#fileupload .error-block').visible - - def wait_for_filename_error(self): - """ - Wait for the upload field to display an error. - """ - EmptyPromise(self.is_filename_error_showing, 'Upload Error Displayed', timeout=30).fulfill() - - def finished_target_url(self): - """ - Grab the URL of the 'view updated library/course outline' button. - """ - return self.q(css='.action.action-primary')[0].get_attribute('href') - - -class ImportCoursePage(ImportMixin, TemplateCheckMixin, CoursePage): - """ - Import page for Courses - """ - - -class ImportLibraryPage(ImportMixin, TemplateCheckMixin, LibraryLoader, LibraryPage): - """ - Import page for Libraries - """ diff --git a/common/test/acceptance/pages/studio/index.py b/common/test/acceptance/pages/studio/index.py deleted file mode 100644 index f34f2525ed..0000000000 --- a/common/test/acceptance/pages/studio/index.py +++ /dev/null @@ -1,389 +0,0 @@ -""" -Studio Index, home and dashboard pages. These are the starting pages for users. -""" - - -from bok_choy.page_object import PageObject -from selenium.webdriver import ActionChains -from selenium.webdriver.common.keys import Keys - -from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.login import LoginPage -from common.test.acceptance.pages.studio.signup import SignupPage -from common.test.acceptance.pages.studio.utils import HelpMixin - - -class HeaderMixin(object): - """ - Mixin class used for the pressing buttons in the header. - """ - def click_sign_up(self): - """ - Press the Sign Up button in the header. - """ - next_page = SignupPage(self.browser) - self.q(css='.action-signup')[0].click() - return next_page.wait_for_page() - - def click_sign_in(self): - """ - Press the Sign In button in the header. - """ - next_page = LoginPage(self.browser) - self.q(css='.action-signin')[0].click() - return next_page.wait_for_page() - - -class IndexPage(PageObject, HeaderMixin, HelpMixin): - """ - Home page for Studio when not logged in. - """ - url = BASE_URL + "/" - - def is_browser_on_page(self): - return self.q(css='.wrapper-text-welcome').visible - - -class DashboardPage(PageObject, HelpMixin): - """ - Studio Dashboard page with courses. - The user must be logged in to access this page. - """ - url = BASE_URL + "/course/" - - def is_browser_on_page(self): - return self.q(css='.content-primary').visible - - @property - def course_runs(self): - """ - The list of course run metadata for all displayed courses - Returns an empty string if there are none - """ - return self.q(css='.course-run>.value').text - - @property - def has_processing_courses(self): - return self.q(css='.courses-processing').present - - def create_rerun(self, course_key): - """ - Clicks the create rerun link of the course specified by course_key - 'Re-run course' link doesn't show up until you mouse over that course in the course listing - """ - actions = ActionChains(self.browser) - button_name = self.browser.find_element_by_css_selector('.rerun-button[href$="' + course_key + '"]') - actions.move_to_element(button_name) - actions.click(button_name) - actions.perform() - - def click_course_run(self, run): - """ - Clicks on the course with run given by run. - """ - self.q(css='.course-run .value').filter(lambda el: el.text == run)[0].click() - # Clicking on course with run will trigger an ajax event - self.wait_for_ajax() - - def scroll_to_course(self, course_key): - """ - Scroll down to the course element - """ - element = '[data-course-key*="{}"]'.format(course_key) - self.scroll_to_element(element) - - def has_new_library_button(self): - """ - (bool) is the "New Library" button present? - """ - return self.q(css='.new-library-button').present - - def click_new_library(self): - """ - Click on the "New Library" button - """ - self.q(css='.new-library-button').first.click() - self.wait_for_ajax() - - def is_new_library_form_visible(self): - """ - Is the new library form visisble? - """ - return self.q(css='.wrapper-create-library').visible - - def fill_new_library_form(self, display_name, org, number): - """ - Fill out the form to create a new library. - Must have called click_new_library() first. - """ - field = lambda fn: self.q(css=u'.wrapper-create-library #new-library-{}'.format(fn)) - field('name').fill(display_name) - field('org').fill(org) - field('number').fill(number) - - def is_new_library_form_valid(self): - """ - Is the new library form ready to submit? - """ - return ( - self.q(css='.wrapper-create-library .new-library-save:not(.is-disabled)').present and - not self.q(css='.wrapper-create-library .wrap-error.is-shown').present - ) - - def submit_new_library_form(self): - """ - Submit the new library form. - """ - self.q(css='.wrapper-create-library .new-library-save').click() - - @property - def new_course_button(self): - """ - Returns "New Course" button. - """ - return self.q(css='.new-course-button') - - def is_new_course_form_visible(self): - """ - Is the new course form visible? - """ - return self.q(css='.wrapper-create-course').visible - - def click_new_course_button(self): - """ - Click "New Course" button - """ - self.q(css='.new-course-button').first.click() - self.wait_for_ajax() - - def fill_new_course_form(self, display_name, org, number, run): - """ - Fill out the form to create a new course. - """ - field = lambda fn: self.q(css=u'.wrapper-create-course #new-course-{}'.format(fn)) - field('name').fill(display_name) - field('org').fill(org) - field('number').fill(number) - field('run').fill(run) - - def is_new_course_form_valid(self): - """ - Returns `True` if new course form is valid otherwise `False`. - """ - return ( - self.q(css='.wrapper-create-course .new-course-save:not(.is-disabled)').present and - not self.q(css='.wrapper-create-course .wrap-error.is-shown').present - ) - - def submit_new_course_form(self): - """ - Submit the new course form. - """ - self.q(css='.wrapper-create-course .new-course-save').first.click() - self.wait_for_ajax() - - @property - def error_notification(self): - """ - Returns error notification element. - """ - return self.q(css='.wrapper-notification-error.is-shown') - - @property - def error_notification_message(self): - """ - Returns text of error message. - """ - self.wait_for_element_visibility( - ".wrapper-notification-error.is-shown .message", "Error message is visible" - ) - return self.error_notification.results[0].find_element_by_css_selector('.message').text - - @property - def course_org_field(self): - """ - Returns course organization input. - """ - return self.q(css='.wrapper-create-course #new-course-org') - - def select_item_in_autocomplete_widget(self, item_text): - """ - Selects item in autocomplete where text of item matches item_text. - """ - self.wait_for_element_visibility( - ".ui-autocomplete .ui-menu-item", "Autocomplete widget is visible" - ) - self.q(css='.ui-autocomplete .ui-menu-item a').filter(lambda el: el.text == item_text)[0].click() - - def list_courses(self, archived=False): - """ - List all the courses found on the page's list of courses. - """ - # Workaround Selenium/Firefox bug: `.text` property is broken on invisible elements - tab_selector = u'#course-index-tabs .{} a'.format('archived-courses-tab' if archived else 'courses-tab') - self.wait_for_element_presence(tab_selector, "Courses Tab") - self.q(css=tab_selector).click() - div2info = lambda element: { - 'name': element.find_element_by_css_selector('.course-title').text, - 'org': element.find_element_by_css_selector('.course-org .value').text, - 'number': element.find_element_by_css_selector('.course-num .value').text, - 'run': element.find_element_by_css_selector('.course-run .value').text, - 'url': element.find_element_by_css_selector('a.course-link').get_attribute('href'), - } - course_list_selector = u'.{} li.course-item'.format('archived-courses' if archived else 'courses') - return self.q(css=course_list_selector).map(div2info).results - - def has_course(self, org, number, run, archived=False): - """ - Returns `True` if course for given org, number and run exists on the page otherwise `False` - """ - for course in self.list_courses(archived): - if course['org'] == org and course['number'] == number and course['run'] == run: - return True - return False - - def list_libraries(self): - """ - Click the tab to display the available libraries, and return detail of them. - """ - # Workaround Selenium/Firefox bug: `.text` property is broken on invisible elements - library_tab_css = '#course-index-tabs .libraries-tab' - self.wait_for_element_presence(library_tab_css, "Libraries tab") - self.q(css=library_tab_css).click() - if self.q(css='.list-notices.libraries-tab').present: - # No libraries are available. - self.wait_for_element_presence('.libraries-tab .new-library-button', "new library tab") - return [] - div2info = lambda element: { - 'name': element.find_element_by_css_selector('.course-title').text, - 'link_element': element.find_element_by_css_selector('.course-title'), - 'org': element.find_element_by_css_selector('.course-org .value').text, - 'number': element.find_element_by_css_selector('.course-num .value').text, - 'url': element.find_element_by_css_selector('a.library-link').get_attribute('href'), - } - self.wait_for_element_visibility('.libraries li.course-item', "Switch to library tab") - return self.q(css='.libraries li.course-item').map(div2info).results - - def has_library(self, **kwargs): - """ - Does the page's list of libraries include a library matching kwargs? - """ - for lib in self.list_libraries(): - if all([lib[key] == kwargs[key] for key in kwargs]): - return True - return False - - def click_library(self, name): - """ - Click on the library with the given name. - """ - for lib in self.list_libraries(): - if lib['name'] == name: - lib['link_element'].click() - - @property - def language_selector(self): - """ - return language selector - """ - self.wait_for_element_visibility( - '#settings-language-value', - 'Language selector element is available' - ) - return self.q(css='#settings-language-value') - - @property - def course_creation_error_message(self): - """ - Returns the course creation error - """ - self.wait_for_element_visibility( - '#course_creation_error>p', - 'Length error is present' - ) - return self.q(css='#course_creation_error>p').text[0] - - def is_create_button_disabled(self): - """ - Returns: True if Create button is disbaled - """ - self.wait_for_element_presence( - '.action.action-primary.new-course-save.is-disabled', - "Create button is disabled" - ) - return True - - -class HomePage(DashboardPage): - """ - Home page for Studio when logged in. - """ - url = BASE_URL + "/home/" - - -class AccessibilityPage(IndexPage): - """ - Home page for Studio when logged in. - """ - url = BASE_URL + "/accessibility" - - def is_browser_on_page(self): - """ - Is the page header visible? - """ - return self.q(css='#root h2').visible - - def header_text_on_page(self): - """ - Check that the page header has the right text. - """ - return 'Individualized Accessibility Process for Course Creators' in self.q(css='#root h2').text - - def fill_form(self, email, name, message): - """ - Fill the accessibility feedback form out. - """ - email_input = self.q(css='#root input#email') - name_input = self.q(css='#root input#fullName') - message_input = self.q(css='#root textarea#message') - - email_input.fill(email) - name_input.fill(name) - message_input.fill(message) - - # Tab off the message textarea to trigger any error messages - message_input[0].send_keys(Keys.TAB) - - def submit_form(self): - """ - Click the submit button on the accessibiltiy feedback form. - """ - button = self.q(css='#root section button')[0] - button.click() - self.wait_for_element_visibility('#root div.alert-dialog', 'Form submission alert is visible') - - def leave_field_blank(self, field_id, field_type='input'): - """ - To simulate leaving a field blank, click on the field, then press TAB to move off focus off the field. - """ - field = self.q(css=u'#root {}#{}'.format(field_type, field_id))[0] - field.click() - field.send_keys(Keys.TAB) - - def alert_has_text(self, text=''): - """ - Check that the alert dialog contains the specified text. - """ - return text in self.q(css='#root div.alert-dialog').text - - def error_message_is_shown_with_text(self, field_id, text=''): - """ - Check that at least one error message is shown and at least one contains the specified text. - """ - selector = u'#root div#error-{}'.format(field_id) - self.wait_for_element_visibility(selector, 'An error message is visible') - error_messages = self.q(css=selector) - for message in error_messages: - if text in message.text: - return True - return False diff --git a/common/test/acceptance/pages/studio/library.py b/common/test/acceptance/pages/studio/library.py index 26d451c1bf..6a931dca22 100644 --- a/common/test/acceptance/pages/studio/library.py +++ b/common/test/acceptance/pages/studio/library.py @@ -4,19 +4,13 @@ Library edit page in Studio import six -from bok_choy.javascript import js_defined, wait_for_js -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.select import Select -from common.test.acceptance.pages.common.utils import confirm_prompt, sync_on_notification +from bok_choy.page_object import PageObject + from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.container import XBlockWrapper from common.test.acceptance.pages.studio.pagination import PaginatedMixin from common.test.acceptance.pages.studio.users import UsersPageMixin from common.test.acceptance.pages.studio.utils import HelpMixin -from common.test.acceptance.pages.studio.xblock_editor import XBlockEditorView class LibraryPage(PageObject, HelpMixin): @@ -46,12 +40,6 @@ class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin): Library edit page in Studio """ - def get_header_title(self): - """ - The text of the main heading (H1) visible on the page. - """ - return self.q(css='h1.page-header-title').text - def wait_until_ready(self): """ When the page first loads, there is a loading indicator and most @@ -63,193 +51,3 @@ class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin): """ self.wait_for_ajax() super(LibraryEditPage, self).wait_until_ready() - - @property - def xblocks(self): - """ - Return a list of xblocks loaded on the container page. - """ - return self._get_xblocks() - - def are_previews_showing(self): - """ - Determines whether or not previews are showing for XBlocks - """ - return all([not xblock.is_placeholder() for xblock in self.xblocks]) - - def toggle_previews(self): - """ - Clicks the preview toggling button and waits for the previews to appear or disappear. - """ - toggle = not self.are_previews_showing() - self.q(css='.toggle-preview-button').click() - EmptyPromise( - lambda: self.are_previews_showing() == toggle, - u'Preview is visible: %s' % toggle, - timeout=30 - ).fulfill() - self.wait_until_ready() - - def click_duplicate_button(self, xblock_id): - """ - Click on the duplicate button for the given XBlock - """ - self._action_btn_for_xblock_id(xblock_id, "duplicate").click() - sync_on_notification(self) - self.wait_for_ajax() - - def click_delete_button(self, xblock_id, confirm=True): - """ - Click on the delete button for the given XBlock - """ - self._action_btn_for_xblock_id(xblock_id, "delete").click() - if confirm: - confirm_prompt(self) # this will also sync_on_notification() - self.wait_for_ajax() - - def _get_xblocks(self): - """ - Create an XBlockWrapper for each XBlock div found on the page. - """ - prefix = '.wrapper-xblock.level-page ' - return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map( - lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator')) - ).results - - def _div_for_xblock_id(self, xblock_id): - """ - Given an XBlock's usage locator as a string, return the WebElement for - that block's wrapper div. - """ - return self.q(css='.wrapper-xblock.level-page .studio-xblock-wrapper').filter( - lambda el: el.get_attribute('data-locator') == xblock_id - ) - - def _action_btn_for_xblock_id(self, xblock_id, action): - """ - Given an XBlock's usage locator as a string, return one of its action - buttons. - action is 'edit', 'duplicate', or 'delete' - """ - return self._div_for_xblock_id(xblock_id)[0].find_element_by_css_selector( - u'.header-actions .{action}-button.action-button'.format(action=action) - ) - - -class StudioLibraryContentEditor(XBlockEditorView): - """ - Library Content XBlock Modal edit window - """ - # Labels used to identify the fields on the edit modal: - LIBRARY_LABEL = "Library" - COUNT_LABEL = "Count" - PROBLEM_TYPE_LABEL = "Problem Type" - - @property - def library_name(self): - """ Gets name of library """ - return self.get_selected_option_text(self.LIBRARY_LABEL) - - @library_name.setter - def library_name(self, library_name): - """ - Select a library from the library select box - """ - self.set_select_value(self.LIBRARY_LABEL, library_name) - EmptyPromise(lambda: self.library_name == library_name, "library_name is updated in modal.").fulfill() - - @property - def count(self): - """ - Gets value of children count input - """ - return int(self.get_setting_element(self.COUNT_LABEL).get_attribute('value')) - - @count.setter - def count(self, count): - """ - Sets value of children count input - """ - count_text = self.get_setting_element(self.COUNT_LABEL) - count_text.send_keys(Keys.CONTROL, "a") - count_text.send_keys(Keys.BACK_SPACE) - count_text.send_keys(count) - EmptyPromise(lambda: self.count == count, "count is updated in modal.").fulfill() - - @property - def capa_type(self): - """ - Gets value of CAPA type select - """ - return self.get_setting_element(self.PROBLEM_TYPE_LABEL).get_attribute('value') - - @capa_type.setter - def capa_type(self, value): - """ - Sets value of CAPA type select - """ - self.set_select_value(self.PROBLEM_TYPE_LABEL, value) - EmptyPromise(lambda: self.capa_type == value, "problem type is updated in modal.").fulfill() - - def set_select_value(self, label, value): - """ - Sets the select with given label (display name) to the specified value - """ - elem = self.get_setting_element(label) - select = Select(elem) - select.select_by_value(value) - - -@js_defined('window.LibraryContentAuthorView') -class StudioLibraryContainerXBlockWrapper(XBlockWrapper): - """ - Wraps :class:`.container.XBlockWrapper` for use with LibraryContent blocks - """ - url = None - - def is_browser_on_page(self): - """ - Returns true iff the library content area has been loaded - """ - return self.q(css='article.content-primary').visible - - def is_finished_loading(self): - """ - Returns true iff the Loading indicator is not visible - """ - return not self.q(css='div.ui-loading').visible - - @classmethod - def from_xblock_wrapper(cls, xblock_wrapper): - """ - Factory method: creates :class:`.StudioLibraryContainerXBlockWrapper` from :class:`.container.XBlockWrapper` - """ - return cls(xblock_wrapper.browser, xblock_wrapper.locator) - - def get_body_paragraphs(self): - """ - Gets library content body paragraphs - """ - return self.q(css=self._bounded_selector(".xblock-message-area p")) - - @wait_for_js # Wait for the fragment.initialize_js('LibraryContentAuthorView') call to finish - def refresh_children(self): - """ - Click "Update now..." button - """ - btn_selector = self._bounded_selector(".library-update-btn") - self.wait_for_element_presence(btn_selector, 'Update now button is present.') - self.q(css=btn_selector).first.click() - - # This causes a reload (see cms/static/xmodule_js/public/js/library_content_edit.js) - # Check that the ajax request that caused the reload is done. - self.wait_for_ajax() - # Then check that we are still on the right page. - self.wait_for(lambda: self.is_browser_on_page(), 'StudioLibraryContainerXBlockWrapper has reloaded.') - # Wait longer than the default 60 seconds, because this was intermittently failing on jenkins - # with the screenshot showing that the Loading indicator was still visible. See TE-745. - self.wait_for(lambda: self.is_finished_loading(), 'Loading indicator is not visible.', timeout=120) - - # And wait to make sure the ajax post has finished. - self.wait_for_ajax() - self.wait_for_element_absence(btn_selector, 'Wait for the XBlock to finish reloading') diff --git a/common/test/acceptance/pages/studio/login.py b/common/test/acceptance/pages/studio/login.py deleted file mode 100644 index 4d81510562..0000000000 --- a/common/test/acceptance/pages/studio/login.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Login page for Studio. -""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.pages.studio import LMS_URL -from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.pages.studio.utils import HelpMixin - - -class LoginMixin(object): - """ - Mixin class used for logging into the system. - """ - def fill_password(self, password): - """ - Fill the password field with the value. - """ - self.q(css="#login-password").fill(password) - - def login(self, email, password, expect_success=True): - """ - Attempt to log in using 'email' and 'password'. - """ - self.wait_for_element_visibility('#login-email', 'Email field is shown') - self.q(css="#login-email").fill(email) - self.fill_password(password) - self.q(css=".login-button").click() - - # Ensure that we make it to another page - if expect_success: - EmptyPromise( - lambda: "login" not in self.browser.current_url, - "redirected from the login page" - ).fulfill() - - -class LoginPage(PageObject, LoginMixin, HelpMixin): - """ - Login page for Studio. - """ - url = LMS_URL + "/login" - - def is_browser_on_page(self): - return ( - self.q(css="#login-anchor").is_present() and - self.q(css=".login-button").visible - ) - - -class CourseOutlineSignInRedirectPage(CoursePage, LoginMixin): - """ - Page shown when the user tries to access the course while not signed in. - """ - url_path = "course" - - def is_browser_on_page(self): - return self.q(css=".login-button").visible diff --git a/common/test/acceptance/pages/studio/move_xblock.py b/common/test/acceptance/pages/studio/move_xblock.py deleted file mode 100644 index bca22f47ec..0000000000 --- a/common/test/acceptance/pages/studio/move_xblock.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Move XBlock Modal Page Object -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.common.utils import click_css - - -class MoveModalView(PageObject): - """ - A base class for move xblock - """ - - def is_browser_on_page(self): - return self.q(css='.modal-window.move-modal').present - - def url(self): - """ - Returns None because this is not directly accessible via URL. - """ - return None - - def save(self): - """ - Clicks save button. - """ - click_css(self, 'a.action-save') - - def cancel(self): - """ - Clicks cancel button. - """ - click_css(self, 'a.action-cancel', require_notification=False) - - def click_forward_button(self, source_index): - """ - Click forward button at specified `source_index`. - """ - css = '.move-modal .xblock-items-container .xblock-item' - self.q(css='.button-forward').nth(source_index).click() - self.wait_for( - lambda: len(self.q(css=css).results) > 0, description='children are visible' - ) - - def click_move_button(self): - """ - Click move button. - """ - self.q(css='.modal-actions .action-move').first.click() - - @property - def is_move_button_enabled(self): - """ - Returns True if move button on modal is enabled else False. - """ - return not self.q(css='.modal-actions .action-move.is-disabled').present - - @property - def children_category(self): - """ - Get displayed children category. - """ - return self.q(css='.xblock-items-container').attrs('data-items-category')[0] - - def navigate_to_category(self, category, navigation_options): - """ - Navigates to specifec `category` for a specified `source_index`. - """ - child_category = self.children_category - while child_category != category: - self.click_forward_button(navigation_options[child_category]) - child_category = self.children_category diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py index 6a3d66c3c5..f897922700 100644 --- a/common/test/acceptance/pages/studio/overview.py +++ b/common/test/acceptance/pages/studio/overview.py @@ -3,21 +3,13 @@ Course Outline page in Studio. """ -import datetime - from bok_choy.javascript import js_defined, wait_for_js from bok_choy.page_object import PageObject from bok_choy.promise import EmptyPromise -from selenium.webdriver import ActionChains -from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import Select -from six.moves import map, range -from common.test.acceptance.pages.common.utils import click_css, confirm_prompt from common.test.acceptance.pages.studio.container import ContainerPage from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.pages.studio.utils import set_input_value, set_input_value_and_save -from common.test.acceptance.tests.helpers import disable_animations, enable_animations, select_option_by_text @js_defined('jQuery') @@ -61,115 +53,6 @@ class CourseOutlineItem(object): else: return selector - @property - def name(self): - """ - Returns the display name of this object. - """ - name_element = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).first # pylint: disable=no-member - if name_element: - return name_element.text[0] - else: - return None - - @property - def has_status_message(self): - """ - Returns True if the item has a status message, False otherwise. - """ - return self.q(css=self._bounded_selector(self.STATUS_MESSAGE_SELECTOR)).first.visible # pylint: disable=no-member - - @property - def status_message(self): - """ - Returns the status message of this item. - """ - selector = self._bounded_selector(self.STATUS_MESSAGE_SELECTOR) - return self.q(css=selector).text[0] # pylint: disable=no-member - - @property - def has_staff_lock_warning(self): - """ Returns True if the 'Contains staff only content' message is visible """ - return self.status_message == 'Contains staff only content' if self.has_status_message else False - - @property - def has_restricted_warning(self): - """ Returns True if the 'Access to this unit is restricted to' message is visible """ - return 'Access to this unit is restricted to' in self.status_message if self.has_status_message else False - - @property - def is_staff_only(self): - """ Returns True if the visiblity state of this item is staff only (has a black sidebar) """ - return "is-staff-only" in self.q(css=self._bounded_selector(''))[0].get_attribute("class") # pylint: disable=no-member - - def edit_name(self): - """ - Puts the item's name into editable form. - """ - self.q(css=self._bounded_selector(self.EDIT_BUTTON_SELECTOR)).first.click() # pylint: disable=no-member - - def enter_name(self, new_name): - """ - Enters new_name as the item's display name. - """ - set_input_value(self, self._bounded_selector(self.NAME_INPUT_SELECTOR), new_name) - - def change_name(self, new_name): - """ - Changes the container's name. - """ - self.edit_name() - set_input_value_and_save(self, self._bounded_selector(self.NAME_INPUT_SELECTOR), new_name) - self.wait_for_ajax() # pylint: disable=no-member - - def finalize_name(self): - """ - Presses ENTER, saving the value of the display name for this item. - """ - # pylint: disable=no-member - self.q(css=self._bounded_selector(self.NAME_INPUT_SELECTOR)).results[0].send_keys(Keys.ENTER) - self.wait_for_ajax() - - def set_staff_lock(self, is_locked): - """ - Sets the explicit staff lock of item on the container page to is_locked. - """ - modal = self.edit() - modal.is_explicitly_locked = is_locked - modal.save() - - def get_enrollment_select_options(self): - """ - Gets the option names available for unit group access - """ - modal = self.edit() - group_options = self.q(css='.group-select-title option').text - modal.cancel() - return group_options - - def toggle_unit_access(self, partition_name, group_ids): - """ - Toggles unit access to the groups in group_ids - """ - if group_ids: - modal = self.edit() - groups_select = self.q(css='.group-select-title select') - select_option_by_text(groups_select, partition_name) - - for group_id in group_ids: - checkbox = self.q(css=u'#content-group-{group_id}'.format(group_id=group_id)) - checkbox.click() - modal.save() - - def in_editable_form(self): - """ - Return whether this outline item's display name is in its editable form. - """ - # pylint: disable=no-member - return "is-editing" in self.q( - css=self._bounded_selector(self.NAME_FIELD_WRAPPER_SELECTOR) - )[0].get_attribute("class") - def edit(self): """ Puts the item into editable form. @@ -182,47 +65,6 @@ class CourseOutlineItem(object): EmptyPromise(lambda: modal.is_shown(), 'Modal is shown.') # pylint: disable=unnecessary-lambda return modal - @property - def release_date(self): - """ - Returns the release date from the page. Date is "mm/dd/yyyy" string. - """ - element = self.q(css=self._bounded_selector(".status-release-value")) # pylint: disable=no-member - return element.first.text[0] if element.present else None - - @property - def due_date(self): - """ - Returns the due date from the page. Date is "mm/dd/yyyy" string. - """ - element = self.q(css=self._bounded_selector(".status-grading-date")) # pylint: disable=no-member - return element.first.text[0] if element.present else None - - @property - def policy(self): - """ - Select the grading format with `value` in the drop-down list. - """ - element = self.q(css=self._bounded_selector(".status-grading-value")) # pylint: disable=no-member - return element.first.text[0] if element.present else None - - @wait_for_js - def publish(self): - """ - Publish the unit. - """ - click_css(self, self._bounded_selector('.action-publish'), require_notification=False) - modal = CourseOutlineModal(self) - EmptyPromise(lambda: modal.is_shown(), 'Modal is shown.') # pylint: disable=unnecessary-lambda - modal.publish() - - @property - def publish_action(self): - """ - Returns the link for publishing a unit. - """ - return self.q(css=self._bounded_selector('.action-publish')).first # pylint: disable=no-member - class CourseOutlineContainer(CourseOutlineItem): """ @@ -234,23 +76,6 @@ class CourseOutlineContainer(CourseOutlineItem): CHILD_CLASS = None ADD_BUTTON_SELECTOR = '> .outline-content > .add-item a.button-new' - def child(self, title, child_class=None): - """ - - :type self: object - """ - if not child_class: - child_class = self.CHILD_CLASS - - # pylint: disable=no-member - return child_class( - self.browser, - self.q(css=child_class.BODY_SELECTOR).filter( - lambda el: title in [inner.text for inner in - el.find_elements_by_css_selector(child_class.NAME_SELECTOR)] - ).attrs('data-locator')[0] - ) - def children(self, child_class=None): """ Returns all the children page objects of class child_class. @@ -271,61 +96,6 @@ class CourseOutlineContainer(CourseOutlineItem): return self.children(child_class)[index] - def add_child(self, require_notification=True): - """ - Adds a child to this xblock, waiting for notifications. - """ - click_css( - self, - self._bounded_selector(self.ADD_BUTTON_SELECTOR), - require_notification=require_notification, - ) - - def expand_subsection(self): - """ - Toggle the expansion of this subsection. - """ - disable_animations(self) - - def subsection_expanded(): - """ - Returns whether or not this subsection is expanded. - """ - self.wait_for_element_presence( - self._bounded_selector(self.ADD_BUTTON_SELECTOR), 'Toggle control is present' - ) - css_element = self._bounded_selector(self.ADD_BUTTON_SELECTOR) - add_button = self.q(css=css_element).first.results # pylint: disable=no-member - self.scroll_to_element(css_element) # pylint: disable=no-member - return add_button and add_button[0].is_displayed() - - currently_expanded = subsection_expanded() - - # Need to click slightly off-center in order for the click to be recognized. - css_element = self._bounded_selector('.ui-toggle-expansion .fa') - self.scroll_to_element(css_element) # pylint: disable=no-member - ele = self.browser.find_element_by_css_selector(css_element) # pylint: disable=no-member - ActionChains(self.browser).move_to_element_with_offset(ele, 8, 8).click().perform() # pylint: disable=no-member - self.wait_for_element_presence(self._bounded_selector(self.ADD_BUTTON_SELECTOR), u'Subsection is expanded') - - EmptyPromise( - lambda: subsection_expanded() != currently_expanded, - u"Check that the container {} has been toggled".format(self.locator) - ).fulfill() - - enable_animations(self) - - return self - - @property - def is_collapsed(self): - """ - Return whether this outline item is currently collapsed. - """ - css_element = self._bounded_selector('') - self.scroll_to_element(css_element) # pylint: disable=no-member - return "is-collapsed" in self.q(css=css_element).first.attrs("class")[0] # pylint: disable=no-member - class CourseOutlineChild(PageObject, CourseOutlineItem): """ @@ -341,13 +111,6 @@ class CourseOutlineChild(PageObject, CourseOutlineItem): def is_browser_on_page(self): return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present - def delete(self, cancel=False): - """ - Clicks the delete button, then cancels at the confirmation prompt if cancel is True. - """ - click_css(self, self._bounded_selector('.delete-button'), require_notification=False) - confirm_prompt(self, cancel) - def _bounded_selector(self, selector): """ Return `selector`, but limited to this particular `CourseOutlineChild` context @@ -358,30 +121,6 @@ class CourseOutlineChild(PageObject, CourseOutlineItem): selector ) - @property - def name(self): - titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text - if titles: - return titles[0] - else: - return None - - @property - def children(self): - """ - Will return any first-generation descendant items of this item. - """ - descendants = self.q(css=self._bounded_selector(self.BODY_SELECTOR)).map( - lambda el: CourseOutlineChild(self.browser, el.get_attribute('data-locator'))).results - - # Now remove any non-direct descendants. - grandkids = [] - for descendant in descendants: - grandkids.extend(descendant.children) - - grand_locators = [grandkid.locator for grandkid in grandkids] - return [descendant for descendant in descendants if descendant.locator not in grand_locators] - class CourseOutlineUnit(CourseOutlineChild): """ @@ -502,89 +241,12 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): self.q(css='div.ui-loading.is-hidden').present ]) - def click_course_status_section_start_date_link(self): - self.course_start_date_link.click() - - def click_course_status_section_checklists_link(self): - self.course_checklists_link.click() - - def view_live(self): - """ - Clicks the "View Live" link and switches to the new tab - """ - click_css(self, '.view-live-button', require_notification=False) - self.wait_for_page() - self.browser.switch_to_window(self.browser.window_handles[-1]) - - def section(self, title): - """ - Return the :class:`.CourseOutlineSection` with the title `title`. - """ - return self.child(title) - def section_at(self, index): """ Returns the :class:`.CourseOutlineSection` at the specified index. """ return self.child_at(index) - def click_section_name(self, parent_css=''): - """ - Find and click on first section name in course outline - """ - self.q(css=u'{} .section-name'.format(parent_css)).first.click() - - def get_section_name(self, parent_css='', page_refresh=False): - """ - Get the list of names of all sections present - """ - if page_refresh: - self.browser.refresh() - return self.q(css=u'{} .section-name'.format(parent_css)).text - - def section_name_edit_form_present(self, parent_css=''): - """ - Check that section name edit form present - """ - return self.q(css=u'{} .section-name input'.format(parent_css)).present - - def change_section_name(self, new_name, parent_css=''): - """ - Change section name of first section present in course outline - """ - self.click_section_name(parent_css) - self.q(css=u'{} .section-name input'.format(parent_css)).first.fill(new_name) - self.q(css=u'{} .section-name .save-button'.format(parent_css)).first.click() - self.wait_for_ajax() - - def sections(self): - """ - Returns the sections of this course outline page. - """ - return self.children() - - def add_section_from_top_button(self): - """ - Clicks the button for adding a section which resides at the top of the screen. - """ - click_css(self, '.wrapper-mast nav.nav-actions .button-new') - - def add_section_from_bottom_button(self, click_child_icon=False): - """ - Clicks the button for adding a section which resides at the bottom of the screen. - """ - element_css = self.BOTTOM_ADD_SECTION_BUTTON - if click_child_icon: - element_css += " .fa-plus" - - click_css(self, element_css) - - def toggle_expand_collapse(self): - """ - Toggles whether all sections are expanded or collapsed - """ - self.q(css=self.EXPAND_COLLAPSE_CSS).click() - def start_reindex(self): """ Starts course reindex by clicking reindex button @@ -598,33 +260,6 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): self.q(css=".subsection-header-actions .configure-button").nth(index).click() self.wait_for_element_presence('.course-outline-modal', 'Subsection settings modal is present.') - def change_problem_release_date(self): - """ - Sets a new start date - """ - self.q(css=".subsection-header-actions .configure-button").first.click() - self.q(css="#start_date").fill("01/01/2030") - self.q(css=".action-save").first.click() - self.wait_for_ajax() - - def change_problem_due_date(self, date): - """ - Sets a new due date. - - Expects date to be a string that will be accepted by the input (for example, '01/01/1970') - """ - self.q(css=".subsection-header-actions .configure-button").first.click() - self.q(css="#due_date").fill(date) - self.q(css=".action-save").first.click() - self.wait_for_ajax() - - def select_visibility_tab(self): - """ - Select the advanced settings tab - """ - self.q(css=".settings-tab-button[data-tab='visibility']").first.click() - self.wait_for_element_presence('input[value=hide_after_due]', 'Visibility fields not present.') - def select_advanced_tab(self, desired_item='special_exam'): """ Select the advanced settings tab @@ -635,285 +270,6 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): if desired_item == 'gated_content': self.wait_for_element_visibility('#is_prereq', 'Gating settings fields are present.') - def make_exam_proctored(self): - """ - Makes a Proctored exam. - """ - self.q(css="input.proctored_exam").first.click() - self.q(css=".action-save").first.click() - self.wait_for_ajax() - - def make_exam_timed(self, hide_after_due=False): - """ - Makes a timed exam. - """ - self.q(css="input.timed_exam").first.click() - if hide_after_due: - self.select_visibility_tab() - self.q(css='input[name=content-visibility][value=hide_after_due]').first.click() - self.q(css=".action-save").first.click() - self.wait_for_ajax() - - def make_subsection_hidden_after_due_date(self): - """ - Sets a subsection to be hidden after due date. - """ - self.q(css='input[value=hide_after_due]').first.click() - self.q(css=".action-save").first.click() - self.wait_for_ajax() - - def select_none_exam(self): - """ - Choose "none" exam but do not press enter - """ - self.q(css="input.no_special_exam").first.click() - - def select_timed_exam(self): - """ - Choose a timed exam but do not press enter - """ - self.q(css="input.timed_exam").first.click() - - def select_proctored_exam(self): - """ - Choose a proctored exam but do not press enter - """ - self.q(css="input.proctored_exam").first.click() - - def select_practice_exam(self): - """ - Choose a practice exam but do not press enter - """ - self.q(css="input.practice_exam").first.click() - - def time_allotted_field_visible(self): - """ - returns whether the time allotted field is visible - """ - return self.q(css=".field-time-limit").visible - - def exam_review_rules_field_visible(self): - """ - Returns whether the review rules field is visible - """ - return self.q(css=".field-exam-review-rules").visible - - def proctoring_items_are_displayed(self): - """ - Returns True if all the items are found. - """ - - # The None radio button - if not self.q(css="input.no_special_exam").present: - return False - - # The Timed exam radio button - if not self.q(css="input.timed_exam").present: - return False - - # The Proctored exam radio button - if not self.q(css="input.proctored_exam").present: - return False - - # The Practice exam radio button - if not self.q(css="input.practice_exam").present: - return False - - return True - - def make_gating_prerequisite(self): - """ - Makes a subsection a gating prerequisite. - """ - if not self.q(css="#is_prereq")[0].is_selected(): - self.q(css='label[for="is_prereq"]').click() - self.q(css=".action-save").first.click() - self.wait_for_ajax() - - def add_prerequisite_to_subsection(self, min_score, min_completion): - """ - Adds a prerequisite to a subsection. - """ - Select(self.q(css="#prereq")[0]).select_by_index(1) - self.q(css="#prereq_min_score").fill(min_score) - self.q(css="#prereq_min_completion").fill(min_completion) - self.q(css=".action-save").first.click() - self.wait_for_ajax() - - def gating_prerequisite_checkbox_is_visible(self): - """ - Returns True if the gating prerequisite checkbox is visible. - """ - - # The Prerequisite checkbox is visible - return self.q(css="#is_prereq").visible - - def gating_prerequisite_checkbox_is_checked(self): - """ - Returns True if the gating prerequisite checkbox is checked. - """ - - # The Prerequisite checkbox is checked - return self.q(css="#is_prereq:checked").present - - def gating_prerequisites_dropdown_is_visible(self): - """ - Returns True if the gating prerequisites dropdown is visible. - """ - - # The Prerequisites dropdown is visible - return self.q(css="#prereq").visible - - def gating_prerequisite_min_score_is_visible(self): - """ - Returns True if the gating prerequisite minimum score input is visible. - """ - - # The Prerequisites dropdown is visible - return self.q(css="#prereq_min_score").visible - - @property - def has_course_status_section(self): - # SFE and SFE-wrapper classes come from studio-frontend and - # wrap content provided by the studio-frontend package - return self.q(css='.course-status .SFE .SFE-wrapper').is_present() - - @property - def course_start_date_link(self): - return self.q(css='.status-link').first - - @property - def course_checklists_link(self): - return self.q(css='.status-link').nth(1) - - @property - def bottom_add_section_button(self): - """ - Returns the query representing the bottom add section button. - """ - return self.q(css=self.BOTTOM_ADD_SECTION_BUTTON).first - - @property - def has_no_content_message(self): - """ - Returns true if a message informing the user that the course has no content is visible - """ - return self.q(css='.outline .no-content').is_present() - - @property - def has_rerun_notification(self): - """ - Returns true iff the rerun notification is present on the page. - """ - return self.q(css='.wrapper-alert.is-shown').is_present() - - def dismiss_rerun_notification(self): - """ - Clicks the dismiss button in the rerun notification. - """ - self.q(css='.dismiss-button').click() - - @property - def expand_collapse_link_state(self): - """ - Returns the current state of the expand/collapse link - """ - link = self.q(css=self.EXPAND_COLLAPSE_CSS)[0] - if not link.is_displayed(): - return ExpandCollapseLinkState.MISSING - elif "collapse-all" in link.get_attribute("class"): - return ExpandCollapseLinkState.COLLAPSE - else: - return ExpandCollapseLinkState.EXPAND - - @property - def reindex_button(self): - """ - Returns reindex button. - """ - return self.q(css=".button.button-reindex")[0] - - def expand_all_subsections(self): - """ - Expands all the subsections in this course. - """ - for section in self.sections(): - if section.is_collapsed: - section.expand_subsection() - for subsection in section.subsections(): - if subsection.is_collapsed: - subsection.expand_subsection() - - @property - def xblocks(self): - """ - Return a list of xblocks loaded on the outline page. - """ - return self.children(CourseOutlineChild) - - @property - def license(self): - """ - Returns the course license text, if present. Else returns None. - """ - return self.q(css=".license-value").first.text[0] - - @property - def deprecated_warning_visible(self): - """ - Returns true if the deprecated warning is visible. - """ - return self.q(css='.wrapper-alert-error.is-shown').is_present() - - @property - def warning_heading_text(self): - """ - Returns deprecated warning heading text. - """ - return self.q(css='.warning-heading-text').text[0] - - @property - def components_list_heading(self): - """ - Returns deprecated warning component list heading text. - """ - return self.q(css='.components-list-heading-text').text[0] - - @property - def modules_remove_text_shown(self): - """ - Returns True if deprecated warning advance modules remove text is visible. - """ - return self.q(css='.advance-modules-remove-text').visible - - @property - def modules_remove_text(self): - """ - Returns deprecated warning advance modules remove text. - """ - return self.q(css='.advance-modules-remove-text').text[0] - - @property - def components_visible(self): - """ - Returns True if components list visible. - """ - return self.q(css='.components-list').visible - - @property - def components_display_names(self): - """ - Returns deprecated warning components display name list. - """ - return self.q(css='.components-list li>a').text - - @property - def deprecated_advance_modules(self): - """ - Returns deprecated advance modules list. - """ - return self.q(css='.advance-modules-list li').text - class CourseOutlineModal(object): """ @@ -957,135 +313,6 @@ class CourseOutlineModal(object): self.click(".action-save") self.page.wait_for_ajax() - def publish(self): - """ - Click the publish action button, and wait for the ajax call to return. - """ - self.click(".action-publish") - self.page.wait_for_ajax() - - def cancel(self): - """ - Click the cancel action button. - """ - self.click(".action-cancel") - - def has_release_date(self): - """ - Check if the input box for the release date exists in the subsection's settings window - """ - return self.find_css("#start_date").present - - def has_release_time(self): - """ - Check if the input box for the release time exists in the subsection's settings window - """ - return self.find_css("#start_time").present - - def has_due_date(self): - """ - Check if the input box for the due date exists in the subsection's settings window - """ - return self.find_css("#due_date").present - - def has_due_time(self): - """ - Check if the input box for the due time exists in the subsection's settings window - """ - return self.find_css("#due_time").present - - def has_policy(self): - """ - Check if the input for the grading policy is present. - """ - return self.find_css("#grading_type").present - - def set_date(self, property_name, input_selector, date): - """ - Set `date` value to input pointed by `selector` and `property_name`. - """ - month, day, year = list(map(int, date.split('/'))) - self.click(input_selector) - if getattr(self, property_name): - current_month, current_year = list(map(int, getattr(self, property_name).split('/')[1:])) - else: # Use default timepicker values, which are current month and year. - current_month, current_year = datetime.datetime.today().month, datetime.datetime.today().year - date_diff = 12 * (year - current_year) + month - current_month - selector = u"a.ui-datepicker-{}".format('next' if date_diff > 0 else 'prev') - for __ in range(abs(date_diff)): - self.page.q(css=selector).click() - self.page.q(css="a.ui-state-default").nth(day - 1).click() # set day - self.page.wait_for_element_invisibility("#ui-datepicker-div", "datepicker should be closed") - EmptyPromise( - lambda: getattr(self, property_name) == u'{m}/{d}/{y}'.format(m=month, d=day, y=year), - u"{} is updated in modal.".format(property_name) - ).fulfill() - - def set_time(self, input_selector, time): - """ - Set `time` value to input pointed by `input_selector` - Not using the time picker to make sure it's not being rounded up - """ - - self.page.q(css=input_selector).fill(time) - self.page.q(css=input_selector).results[0].send_keys(Keys.ENTER) - - @property - def release_date(self): - """ - Returns the unit's release date. Date is "mm/dd/yyyy" string. - """ - return self.find_css("#start_date").first.attrs('value')[0] - - @release_date.setter - def release_date(self, date): - """ - Sets the unit's release date to `date`. Date is "mm/dd/yyyy" string. - """ - self.set_date('release_date', "#start_date", date) - - @property - def release_time(self): - """ - Returns the current value of the release time. Default is u'00:00' - """ - return self.find_css("#start_time").first.attrs('value')[0] - - @release_time.setter - def release_time(self, time): - """ - Time is "HH:MM" string. - """ - self.set_time("#start_time", time) - - @property - def due_date(self): - """ - Returns the due date from the page. Date is "mm/dd/yyyy" string. - """ - return self.find_css("#due_date").first.attrs('value')[0] - - @due_date.setter - def due_date(self, date): - """ - Sets the due date for the unit. Date is "mm/dd/yyyy" string. - """ - self.set_date('due_date', "#due_date", date) - - @property - def due_time(self): - """ - Returns the current value of the release time. Default is u'' - """ - return self.find_css("#due_time").first.attrs('value')[0] - - @due_time.setter - def due_time(self, time): - """ - Time is "HH:MM" string. - """ - self.set_time("#due_time", time) - @property def policy(self): """ @@ -1108,49 +335,6 @@ class CourseOutlineModal(object): "Grading label is updated.", ).fulfill() - @property - def is_staff_lock_visible(self): - """ - Returns True if the staff lock option is visible. - """ - return self.find_css('#staff_lock').visible - - def ensure_staff_lock_visible(self): - """ - Ensures the staff lock option is visible, clicking on the advanced tab - if needed. - """ - if not self.is_staff_lock_visible: - self.find_css(".settings-tab-button[data-tab=visibility]").click() - EmptyPromise( - lambda: self.is_staff_lock_visible, - "Staff lock option is visible", - ).fulfill() - - @property - def is_explicitly_locked(self): - """ - Returns true if the explict staff lock checkbox is checked, false otherwise. - """ - self.ensure_staff_lock_visible() - return self.find_css('#staff_lock')[0].is_selected() - - @is_explicitly_locked.setter - def is_explicitly_locked(self, value): - """ - Checks the explicit staff lock box if value is true, otherwise selects "visible". - """ - self.ensure_staff_lock_visible() - if value != self.is_explicitly_locked: - self.find_css('#staff_lock').click() - EmptyPromise(lambda: value == self.is_explicitly_locked, "Explicit staff lock is updated").fulfill() - - def shows_staff_lock_warning(self): - """ - Returns true iff the staff lock warning is visible. - """ - return self.find_css('.staff-lock .tip-warning').visible - def get_selected_option_text(self, element): """ Returns the text of the first selected option for the element. diff --git a/common/test/acceptance/pages/studio/problem_editor.py b/common/test/acceptance/pages/studio/problem_editor.py deleted file mode 100644 index 2baa55750e..0000000000 --- a/common/test/acceptance/pages/studio/problem_editor.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -Studio Problem Editor -""" - - -from selenium.webdriver.support.ui import Select -from six.moves import range - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.studio.xblock_editor import XBlockEditorView - - -class ProblemXBlockEditorView(XBlockEditorView): - """ - Represents the rendered view of a Problem editor. - """ - - editor_mode_css = '.edit-xblock-modal .editor-modes .editor-button' - settings_mode = '.settings-button' - - def open_settings(self): - """ - Clicks on the settings button - """ - click_css(self, self.settings_mode) - - def set_field_val(self, field_display_name, field_value): - """ - If editing, set the value of a field. - """ - selector = u'.metadata_edit li.field label:contains("{}") + input'.format(field_display_name) - script = "$(arguments[0]).val(arguments[1]).change();" - self.browser.execute_script(script, selector, field_value) - - def get_field_val(self, field_display_name): - """ - If editing, get the value of field - - Args: - field_display_name(str): Name of the field for which the value is required - Returns: - (string): Value of the field - """ - script = u"return $('.wrapper-comp-setting label:contains({}) + input').val();".format(field_display_name) - return self.browser.execute_script(script) - - def get_default_dropdown_value(self, css): - """ - Gets default value from the dropdown - Args: - css(string): css of the dropdown for which default value is required - Returns: - value(string): Default dropdown value - """ - element = self.browser.find_element_by_css_selector(css) - dropdown_default_selection = Select(element) - value = dropdown_default_selection.first_selected_option.text - return value - - def select_from_dropdown(self, dropdown_name, value): - """ - Selects from the dropdown - Arguments: - dropdown_name(string): Name of the dropdown to be opened - value(string): Value to be selected - """ - self.q(css=u'select[class="input setting-input"][name="{}"]'.format(dropdown_name)).first.click() - self.wait_for_element_visibility(u'option[value="{}"]'.format(value), 'Dropdown is visible') - self.q(css=u'option[value="{}"]'.format(value)).click() - - def get_value_from_the_dropdown(self, dropdown_name): - """ - Get selected value from the dropdown - Args: - dropdown_name(string): Name of the dropdown - Returns: - (string): Selected Value from the dropdown - - """ - dropdown = self.browser.find_element_by_css_selector( - u'select[class="input setting-input"][name="{}"]'.format(dropdown_name) - ) - return Select(dropdown).first_selected_option.text - - def get_settings(self): - """ - Default settings of problem - Returns: - settings_dict(dictionary): A dictionary of all the default settings - """ - settings_dict = {} - number_of_settings = len(self.q(css='.wrapper-comp-setting')) - css = u'.list-input.settings-list .field.comp-setting-entry:nth-of-type({}) {}' - - for index in range(1, number_of_settings + 1): - key = self.q(css=css.format(index, "label")).text[0] - if self.q(css=css.format(index, "input")).present: - value = self.q(css=css.format(index, "input")).attrs('value')[0] - elif self.q(css=css.format(index, "select")).present: - value = self.get_default_dropdown_value(css.format(index, "select")) - settings_dict[key] = value - - return settings_dict - - def revert_setting(self, display_name=False): - """ - Click to revert setting to default - """ - if display_name: - self.q(css='.action.setting-clear.active').first.click() - else: - self.q(css='.action.setting-clear.active').results[1].click() - - def toggle_cheatsheet(self): - """ - Toggle cheatsheet on toolbar - """ - self.q(css='.cheatsheet-toggle').first.click() - self.wait_for_element_visibility('.simple-editor-cheatsheet.shown', 'Cheatsheet is visible') - - def is_cheatsheet_present(self): - """ - Check for cheatsheet presence - Returns: - bool: True if present - """ - return self.q(css='.simple-editor-cheatsheet.shown').present - - def is_latex_compiler_present(self): - """ - Checks for the presence of latex compiler settings - Returns: - bool: True if present - """ - return self.q(css='.launch-latex-compiler').present - - def save(self): - """ - Clicks save button. - """ - click_css(self, '.action-save') diff --git a/common/test/acceptance/pages/studio/settings.py b/common/test/acceptance/pages/studio/settings.py index 77615ed3fb..c5594a6fcf 100644 --- a/common/test/acceptance/pages/studio/settings.py +++ b/common/test/acceptance/pages/studio/settings.py @@ -4,16 +4,10 @@ Course Schedule and Details Settings page. """ -import os -import os.path - -import six from bok_choy.javascript import requirejs -from bok_choy.promise import EmptyPromise from common.test.acceptance.pages.studio.course_page import CoursePage from common.test.acceptance.pages.studio.users import wait_for_ajax_or_reload -from common.test.acceptance.pages.studio.utils import press_the_notification_button, type_in_codemirror @requirejs('js/factories/settings') @@ -33,324 +27,3 @@ class SettingsPage(CoursePage): def is_browser_on_page(self): wait_for_ajax_or_reload(self.browser) return self.q(css='body.view-settings').visible - - def wait_for_require_js(self): - """ - Wait for require-js to load javascript files. - """ - if hasattr(self, 'wait_for_js'): - self.wait_for_js() # pylint: disable=no-member - - def wait_for_jquery_value(self, jquery_element, value): - """ - Use jQuery to obtain the element's value. - This is useful for when jQuery performs functions towards the - end of the page load. (In other words, waiting for jquery to - load is not enough; we need to also query values that it has - injected onto the page to ensure it's done.) - """ - self.wait_for( - lambda: self.browser.execute_script( - "return $('{ele}').val();".format(ele=jquery_element)) == '{val}'.format(val=value), - 'wait for jQuery to finish loading data on page.' - ) - - def refresh_and_wait_for_load(self): - """ - Refresh the page and wait for all resources to load. - """ - self.browser.refresh() - self.wait_for_page() - - def get_elements(self, css_selector): - self.wait_for_element_presence( - css_selector, - 'Elements matching "{}" selector are present'.format(css_selector) - ) - results = self.q(css=css_selector) - return results - - def get_element(self, css_selector): - results = self.get_elements(css_selector=css_selector) - return results[0] if results else None - - def set_element_values(self, element_values): - """ - Set the values of the elements to those specified - in the element_values dict. - """ - for css, value in six.iteritems(element_values): - element = self.get_element(css) - element.clear() - element.send_keys(value) - - def un_focus_input_field(self): - """ - Makes an input field un-focus by - clicking outside of it. - """ - self.get_element('.title-2').click() - - def is_element_present(self, css_selector): - """ - Returns boolean based on the presence - of an element with css as passed. - """ - return self.q(css=css_selector).present - - def change_course_description(self, change_text): - """ - Changes the course description - """ - type_in_codemirror(self, 0, change_text, find_prefix="$") - - ################ - # Properties - ################ - @property - def pre_requisite_course_options(self): - """ - Returns the pre-requisite course drop down field options. - """ - self.wait_for_element_visibility( - '#pre-requisite-course', - 'Prerequisite course element is available' - ) - return self.get_elements('#pre-requisite-course') - - @property - def entrance_exam_field(self): - """ - Returns the enable entrance exam checkbox. - """ - self.wait_for_element_visibility( - '#entrance-exam-enabled', - 'Entrance exam checkbox is available' - ) - return self.get_element('#entrance-exam-enabled') - - @property - def alert_confirmation_title(self): - """ - Returns the alert confirmation element, which contains text - such as 'Your changes have been saved.' - """ - self.wait_for_element_visibility( - '#alert-confirmation-title', - 'Alert confirmation title element is available' - ) - return self.get_element('#alert-confirmation-title') - - @property - def course_license(self): - """ - Property. Returns the text of the license type for the course - ("All Rights Reserved" or "Creative Commons") - """ - license_types_css = ".license ul.license-types li.license-type" - self.wait_for_element_presence( - license_types_css, - "license type buttons are present", - ) - selected = self.q(css=license_types_css + " button.is-selected") - if selected.is_present(): - return selected.text[0] - - # Look for the license text that will be displayed by default, - # if no button is yet explicitly selected - license_text = self.q(css='.license span.license-text') - if license_text.is_present(): - return license_text.text[0] - return None - - @course_license.setter - def course_license(self, license_name): - """ - Sets the course license to the given license_name - (str, "All Rights Reserved" or "Creative Commons") - """ - license_types_css = ".license ul.license-types li.license-type" - self.wait_for_element_presence( - license_types_css, - "license type buttons are present", - ) - button_xpath = ( - "//div[contains(@class, 'license')]" - "//ul[contains(@class, 'license-types')]" - "//li[contains(@class, 'license-type')]" - "//button[contains(text(),'{license_name}')]" - ).format(license_name=license_name) - button = self.q(xpath=button_xpath) - if not button.present: - raise Exception("Invalid license name: {name}".format(name=license_name)) - button.click() - - pacing_css = '.pacing input[type=radio]' - - @property - def checked_pacing_css(self): - """CSS for the course pacing button which is currently checked.""" - return self.pacing_css + ':checked' - - @property - def course_pacing(self): - """ - Returns the label text corresponding to the checked pacing radio button. - """ - self.wait_for_element_presence(self.checked_pacing_css, 'course pacing controls present and rendered') - checked = self.q(css=self.checked_pacing_css).results[0] - checked_id = checked.get_attribute('id') - return self.q(css='label[for={checked_id}]'.format(checked_id=checked_id)).results[0].text - - @course_pacing.setter - def course_pacing(self, pacing): - """ - Sets the course to either self-paced or instructor-paced by checking - the appropriate radio button. - """ - self.wait_for_element_presence(self.checked_pacing_css, 'course pacing controls present') - self.q(xpath="//label[contains(text(), '{pacing}')]".format(pacing=pacing)).click() - - @property - def course_pacing_disabled_text(self): - """ - Return the message indicating that course pacing cannot be toggled. - """ - return self.q(css='#course-pace-toggle-tip').results[0].text - - def course_pacing_disabled(self): - """ - Return True if the course pacing controls are disabled; False otherwise. - """ - self.wait_for_element_presence(self.checked_pacing_css, 'course pacing controls present') - statuses = self.q(css=self.pacing_css).map(lambda e: e.get_attribute('disabled')).results - return all((s == 'true' for s in statuses)) - - ################ - # Waits - ################ - def wait_for_prerequisite_course_options(self): - """ - Ensure the pre_requisite_course_options dropdown selector is displayed - """ - EmptyPromise( - lambda: self.q(css="#pre-requisite-course").present, - 'Prerequisite course dropdown selector is displayed' - ).fulfill() - - ################ - # Clicks - ################ - - def click_button(self, name): - """ - Clicks the button - """ - btn_css = 'div#page-notification button.action-{}'.format(name.lower()) - EmptyPromise( - lambda: self.q(css=btn_css).visible, - '{} button is visible'.format(name) - ).fulfill() - press_the_notification_button(self, name) - - ################ - # Workflows - ################ - - def require_entrance_exam(self, required=True): - """ - Set the entrance exam requirement via the checkbox. - """ - checkbox = self.entrance_exam_field - # Wait for license section to load before interacting with the checkbox to avoid race condition - self.wait_for_element_presence('div.wrapper-license', 'License section present') - selected = checkbox.is_selected() - self.scroll_to_element('#entrance-exam-enabled') - if required and not selected: - checkbox.click() - self.wait_for_element_presence( - '#entrance-exam-minimum-score-pct', - 'Entrance exam minimum score percent is present' - ) - if not required and selected: - checkbox.click() - self.wait_for_element_absence( - '#entrance-exam-minimum-score-pct', - 'Entrance exam minimum score percent is absent' - ) - - def save_changes(self, wait_for_confirmation=True): - """ - Clicks save button, waits for confirmation unless otherwise specified - """ - press_the_notification_button(self, "save") - if wait_for_confirmation: - self.wait_for_element_visibility( - '#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): - """ - Reload the page. - """ - self.browser.refresh() - if wait_for_confirmation: - EmptyPromise( - lambda: self.q(css='body.view-settings').present, - 'Page is refreshed' - ).fulfill() - self.wait_for_require_js() - self.wait_for_ajax() - - @staticmethod - def get_asset_path(file_name): - """ - Returns the full path of the file to upload. - These files have been placed in edx-platform/common/test/data/uploads/ - """ - - # Separate the list of folders in the path reaching to the current file, - # e.g. '... common/test/acceptance/pages/lms/instructor_dashboard.py' will result in - # [..., 'common', 'test', 'acceptance', 'pages', 'lms', 'instructor_dashboard.py'] - folders_list_in_path = os.path.abspath(__file__).split(os.sep) - - # Get rid of the last 4 elements: 'acceptance', 'pages', 'lms', and 'instructor_dashboard.py' - # to point to the 'test' folder, a shared point in the path's tree. - folders_list_in_path = folders_list_in_path[:-4] - - # Append the folders in the asset's path - folders_list_in_path.extend(['data', 'uploads', file_name]) - - # Return the joined path of the required asset. - return os.sep.join(folders_list_in_path) - - def upload_image(self, upload_btn_selector, file_to_upload): - """ - Upload image specified by image_selector and file_to_upload - """ - - # wait for upload button - self.wait_for_element_visibility(upload_btn_selector, 'upload button is present') - - self.q(css=upload_btn_selector).results[0].click() - - # wait for popup - self.wait_for_element_presence(self.upload_image_popup_window_selector, 'upload dialog is present') - - # upload image - filepath = SettingsPage.get_asset_path(file_to_upload) - self.q(css=self.upload_image_browse_button_selector).results[0].send_keys(filepath) - self.q(css=self.upload_image_upload_button_selector).results[0].click() - - # wait for popup closed - self.wait_for_element_absence(self.upload_image_popup_window_selector, 'upload dialog is hidden') - - def get_uploaded_image_path(self, image_selector): - """ - Returns the uploaded image path - """ - - return self.q(css=image_selector).attrs('src')[0] diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py deleted file mode 100644 index 0227729522..0000000000 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ /dev/null @@ -1,273 +0,0 @@ -""" -Course Advanced Settings page -""" - - -import six -from bok_choy.promise import EmptyPromise - -from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.pages.studio.utils import ( - get_codemirror_value, - press_the_notification_button, - type_in_codemirror -) - -KEY_CSS = '.key h3.title' -UNDO_BUTTON_SELECTOR = ".action-item .action-undo" -MANUAL_BUTTON_SELECTOR = ".action-item .action-cancel" -MODAL_SELECTOR = ".validation-error-modal-content" -ERROR_ITEM_NAME_SELECTOR = ".error-item-title strong" -ERROR_ITEM_CONTENT_SELECTOR = ".error-item-message" -SETTINGS_NAME_SELECTOR = ".is-not-editable" -CONFIRMATION_MESSAGE_SELECTOR = "#alert-confirmation-title" -DEPRECATED_SETTINGS_SELECTOR = ".field-group.course-advanced-policy-list-item.is-deprecated" -DEPRECATED_SETTINGS_BUTTON_SELECTOR = ".deprecated-settings-label" - - -class AdvancedSettingsPage(CoursePage): - """ - Course Advanced Settings page. - """ - - url_path = "settings/advanced" - - def is_browser_on_page(self): - def _is_finished_loading(): - return len(self.q(css='.course-advanced-policy-list-item')) > 0 - - EmptyPromise(_is_finished_loading, 'Finished rendering the advanced policy items.').fulfill() - return self.q(css='body.advanced').present - - @property - def key_names(self): - """ - Returns a list of key names of all settings. - """ - return self.q(css=KEY_CSS).text - - @property - def deprecated_settings_button_text(self): - """ - Returns text for deprecated settings button - """ - return self.q(css=DEPRECATED_SETTINGS_BUTTON_SELECTOR).text[0] - - def is_deprecated_setting_visible(self): - """ - Returns true if deprecated settings are visible - """ - return self.q(css=DEPRECATED_SETTINGS_SELECTOR).visible - - def toggle_deprecated_settings(self): - """ - Show deprecated Settings - """ - button_text = self.deprecated_settings_button_text - self.q(css=DEPRECATED_SETTINGS_BUTTON_SELECTOR).click() - if button_text == 'Show Deprecated Settings': - self.wait_for_element_presence(DEPRECATED_SETTINGS_SELECTOR, 'Deprecated Settings are present') - else: - self.wait_for_element_absence(DEPRECATED_SETTINGS_SELECTOR, 'Deprecated Settings are not present') - - def wait_for_modal_load(self): - """ - Wait for validation response from the server, and make sure that - the validation error modal pops up. - - This method should only be called when it is guaranteed that there're - validation errors in the settings changes. - """ - self.wait_for_ajax() - self.wait_for_element_presence(MODAL_SELECTOR, 'Validation Modal is present') - - def refresh_and_wait_for_load(self): - """ - Refresh the page and wait for all resources to load. - """ - self.browser.refresh() - self.wait_for_page() - - @property - def confirmation_message(self): - """ - Returns the text of confirmation message which appears after saving the settings - """ - self.wait_for_element_visibility(CONFIRMATION_MESSAGE_SELECTOR, 'Confirmation message is visible') - return self.q(css=CONFIRMATION_MESSAGE_SELECTOR).text[0] - - def coordinates_for_scrolling(self, coordinates_for): - """ - Get the x and y coordinates of elements - """ - cordinates_dict = self.browser.find_element_by_css_selector(coordinates_for) - location = cordinates_dict.location - for key, val in six.iteritems(location): - if key == 'x': - x_axis = val - elif key == 'y': - y_axis = val - return x_axis, y_axis - - def undo_changes_via_modal(self): - """ - Trigger clicking event of the undo changes button in the modal. - Wait for the undoing process to load via ajax call. - Before that Scroll so the button is clickable on all browsers - """ - self.browser.execute_script("window.scrollTo" + str(self.coordinates_for_scrolling(UNDO_BUTTON_SELECTOR))) - self.q(css=UNDO_BUTTON_SELECTOR).click() - self.wait_for_ajax() - - def trigger_manual_changes(self): - """ - Trigger click event of the manual changes button in the modal. - No need to wait for any ajax. - Before that Scroll so the button is clickable on all browsers - """ - self.browser.execute_script("window.scrollTo" + str(self.coordinates_for_scrolling(MANUAL_BUTTON_SELECTOR))) - self.q(css=MANUAL_BUTTON_SELECTOR).click() - - def is_validation_modal_present(self): - """ - Checks if the validation modal is present. - """ - return self.q(css=MODAL_SELECTOR).present - - def get_error_item_names(self): - """ - Returns a list of display names of all invalid settings. - """ - return self.q(css=ERROR_ITEM_NAME_SELECTOR).text - - def get_error_item_messages(self): - """ - Returns a list of error messages of all invalid settings. - """ - return self.q(css=ERROR_ITEM_CONTENT_SELECTOR).text - - def _get_index_of(self, expected_key): - """ - Returns the index of expected key - """ - for i, element in enumerate(self.q(css=KEY_CSS)): - # Sometimes get stale reference if I hold on to the array of elements - key = self.q(css=KEY_CSS).nth(i).text[0] - if key == expected_key: - return i - - return -1 - - def save(self): - press_the_notification_button(self, "Save") - - def cancel(self): - press_the_notification_button(self, "Cancel") - - def set(self, key, new_value): - index = self._get_index_of(key) - type_in_codemirror(self, index, new_value) - self.save() - - def get(self, key): - index = self._get_index_of(key) - return get_codemirror_value(self, index) - - def set_values(self, key_value_map): - """ - Make multiple settings changes and save them. - """ - for key, value in six.iteritems(key_value_map): - index = self._get_index_of(key) - type_in_codemirror(self, index, value) - - self.save() - - def get_values(self, key_list): - """ - Get a key-value dictionary of all keys in the given list. - """ - result_map = {} - - for key in key_list: - index = self._get_index_of(key) - val = get_codemirror_value(self, index) - result_map[key] = val - - return result_map - - @property - def displayed_settings_names(self): - """ - Returns all settings displayed on the advanced settings page/screen/modal/whatever - We call it 'name', but it's really whatever is embedded in the 'id' element for each field - """ - query = self.q(css=SETTINGS_NAME_SELECTOR) - return query.attrs('id') - - @property - def expected_settings_names(self): - """ - Returns a list of settings expected to be displayed on the Advanced Settings screen - Should match the list of settings found in cms/djangoapps/models/settings/course_metadata.py - If a new setting is added to the metadata list, this test will fail and you must update it. - Basically this guards against accidental exposure of a field on the Advanced Settings screen - """ - return [ - 'advanced_modules', - 'allow_anonymous', - 'allow_anonymous_to_peers', - 'allow_public_wiki_access', - 'cert_html_view_overrides', - 'cert_name_long', - 'cert_name_short', - 'certificates_display_behavior', - 'course_image', - 'banner_image', - 'video_thumbnail_image', - 'cosmetic_display_price', - 'advertised_start', - 'announcement', - 'display_name', - 'is_new', - 'issue_badges', - 'max_student_enrollments_allowed', - 'no_grade', - 'display_coursenumber', - 'display_organization', - 'catalog_visibility', - 'days_early_for_beta', - 'disable_progress_graph', - 'discussion_blackouts', - 'discussion_sort_alpha', - 'discussion_topics', - 'due', - 'due_date_display_format', - 'edxnotes', - 'use_latex_compiler', - 'video_speed_optimizations', - 'enrollment_domain', - 'html_textbooks', - 'invitation_only', - 'lti_passports', - 'matlab_api_key', - 'max_attempts', - 'mobile_available', - 'rerandomize', - 'remote_gradebook', - 'showanswer', - 'show_calculator', - 'show_reset_button', - 'static_asset_path', - 'teams_configuration', - 'social_sharing_url', - 'video_bumper', - 'enable_proctored_exams', - 'allow_proctoring_opt_out', - 'enable_timed_exams', - 'enable_subsection_gating', - 'learning_info', - 'instructor_info', - 'ccx_connector', - 'enable_ccx', - ] diff --git a/common/test/acceptance/pages/studio/settings_certificates.py b/common/test/acceptance/pages/studio/settings_certificates.py deleted file mode 100644 index 33487f3353..0000000000 --- a/common/test/acceptance/pages/studio/settings_certificates.py +++ /dev/null @@ -1,655 +0,0 @@ -""" -Course Certificates page objects. -The methods in these classes are organized into several conceptual buckets: - * Helpers: General utility methods used throughout, such as css selection helpers - * Properties: Specific page/object field getters/setters (mainly for form inputs) - * Wait Actions: EmptyPromises used to ensure element availabilty prior to performing an action - * Click Actions: Specific element invocations -- mainly links/buttons but anything clickable - * Workflows: Complex orchestrations involving any/all of the above - -""" - - -import os -import os.path - -from bok_choy.promise import EmptyPromise -from selenium.webdriver import ActionChains -from six.moves import range - -from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.tests.helpers import disable_animations - - -class CertificatesPage(CoursePage): - """ - Course Certificates page object wrapper - Further below you will also find page objects for Certificates and Signatories - """ - - url_path = "certificates" - certficate_css = ".certificates-list" - - ################ - # Helpers - ################ - - def refresh(self): - """ - Refresh the certificate page - """ - self.browser.refresh() - - def is_browser_on_page(self): - """ - Verify that the browser is on the page and it is not still loading. - """ - EmptyPromise( - lambda: self.q(css='body.view-certificates').present, - 'On the certificates page' - ).fulfill() - - EmptyPromise( - lambda: not self.q(css='span.spin').visible, - 'Certificates are finished loading' - ).fulfill() - - return True - - def get_first_signatory_title(self): - """ - Return signatory title for the first signatory in certificate. - """ - return self.q(css='.signatory-title-value').first.html[0] - - def get_course_number(self): - """ - Return Course Number - """ - return self.q(css='.actual-course-number .certificate-value').first.text[0] - - def get_course_number_override(self): - """ - Return Course Number Override - """ - return self.q(css='.course-number-override .certificate-value').first.text[0] - - def course_number_override(self): - """ - Return Course Number Override selector - """ - return self.q(css='.course-number-override') - - ################ - # Properties - ################ - - @property - def certificates(self): - """ - Return list of the certificates for the course. - """ - css = self.certficate_css + ' .wrapper-collection' - return [CertificateSectionPage(self, self.certficate_css, index) for index in range(len(self.q(css=css)))] - - @property - def no_certificates_message_shown(self): - """ - Returns whether or not no certificates created message is present. - """ - return self.q(css='.wrapper-content ' + self.certficate_css + ' .no-content').present - - @property - def no_certificates_message_text(self): - """ - Returns text of .no-content container. - """ - return self.q(css='.wrapper-content ' + self.certficate_css + ' .no-content').text[0] - - @property - def new_certificate_link_text(self): - """ - Returns text of new-button link . - """ - return self.q(css='.wrapper-content ' + self.certficate_css + ' .no-content a.new-button').text[0] - - ################ - # Wait Actions - ################ - - def wait_for_confirmation_prompt(self): - """ - Show confirmation prompt - We can't use confirm_prompt because its wait_for_notification is flaky when asynchronous operation - completed very quickly. - """ - EmptyPromise( - lambda: self.q(css='.prompt').present, - 'Confirmation prompt is displayed' - ).fulfill() - EmptyPromise( - lambda: self.q(css='.prompt .action-primary').present, - 'Primary button is displayed' - ).fulfill() - EmptyPromise( - lambda: self.q(css='.prompt .action-primary').visible, - 'Primary button is visible' - ).fulfill() - - def wait_for_first_certificate_button(self): - """ - Ensure the button is available for use - """ - EmptyPromise( - lambda: self.q(css=self.certficate_css + " .new-button").present, - 'Create first certificate button is displayed' - ).fulfill() - - def wait_for_add_certificate_button(self): - """ - Ensure the button is available for use - """ - EmptyPromise( - lambda: self.q(css=self.certficate_css + " .action-add").present, - 'Add certificate button is displayed' - ).fulfill() - - ################ - # Click Actions - ################ - - def click_first_certificate_button(self): - """ - Clicks the 'Create your first certificate' button, which is only displayed at zero state - """ - self.wait_for_first_certificate_button() - self.q(css=self.certficate_css + " .new-button").first.click() - - def click_add_certificate_button(self): - """ - Clicks the 'Add new certificate' button, which is displayed when certificates already exist - """ - self.wait_for_add_certificate_button() - self.q(css=self.certficate_css + " .action-add").first.click() - - def click_confirmation_prompt_primary_button(self): - """ - Clicks the main action presented by the prompt (such as 'Delete') - """ - disable_animations(self) - self.wait_for_confirmation_prompt() - self.q(css='.prompt button.action-primary').first.click() - self.wait_for_element_invisibility('.prompt', 'wait for pop up to disappear') - self.wait_for_ajax() - - -class CertificateSectionPage(CertificatesPage): - """ - CertificateSectionPage is the certificate section within Certificates page, There might be multiple certificates - in a Certificates Page so this section object can be used to used to identify unique certificate and apply - operations on it. - """ - - def __init__(self, container, prefix, index): - """ - Initialize CertificateSection Page - - :param container: Container Page Object of the certificate section - :param prefix: css selector of the container element - :param index: index of section in the certificate list on the page - - :return: - """ - self.selector = prefix + u' .certificates-list-item-{}'.format(index) - self.index = index - - super(CertificateSectionPage, self).__init__(container.browser, **container.course_info) - - def is_browser_on_page(self): - """ - Verify that the browser is on the page and it is not still loading. - """ - return self.q(css=".certificates").present - - @property - def url(self): - """ - Construct a URL to the page section within the certificate page. - """ - # This is a page section and can not be accessed directly - return None - - ################ - # Helpers - ################ - - def get_selector(self, css=''): - """ - Return selector fo certificate container - """ - return ' '.join([self.selector, css]) - - def find_css(self, css_selector): - """ - Find elements as defined by css locator. - """ - return self.q(css=self.get_selector(css=css_selector)) - - def get_text(self, css): - """ - Return text for the defined by css locator. - """ - return self.find_css(css).first.text[0] - - ################ - # Properties - ################ - - @property - def validation_message(self): - """ - Return validation message. - """ - return self.get_text('.message-status.error') - - @property - def mode(self): - """ - Return certificate mode. - """ - if self.find_css('.collection-edit').present: - return 'edit' - elif self.find_css('.collection').present: - return 'details' - - @property - # pylint: disable=invalid-name - def id(self): - """ - Returns certificate id. - """ - return self.get_text('.certificate-id .certificate-value') - - @property - def name(self): - """ - Return certificate name. - """ - return self.get_text('.name') - - @name.setter - def name(self, value): - """ - Set certificate name. - """ - self.find_css('.collection-name-input').first.fill(value) - - @property - def description(self): - """ - Return certificate description. - """ - return self.get_text('.certificate-description') - - @description.setter - def description(self, value): - """ - Set certificate description. - """ - self.find_css('.certificate-description-input').first.fill(value) - - @property - def course_title(self): - """ - Return certificate course title override field. - """ - return self.get_text('.course-title-override .certificate-value') - - @course_title.setter - def course_title(self, value): - """ - Set certificate course title override field. - """ - self.find_css('.certificate-course-title-input').first.fill(value) - - @property - def signatories(self): - """ - Return list of the signatories for the certificate. - """ - css = self.selector + ' .signatory-' + self.mode - return [SignatorySectionPage(self, self.selector, self.mode, index) for index in range(len(self.q(css=css)))] - - ################ - # Wait Actions - ################ - - def wait_for_certificate_delete_button(self): - """ - Returns whether or not the certificate delete icon is present. - """ - EmptyPromise( - lambda: self.find_css('.actions .delete.action-icon').present, - 'Certificate delete button is displayed' - ).fulfill() - - def wait_for_hide_details_toggle(self): - """ - Certificate details are expanded. - """ - EmptyPromise( - lambda: self.find_css('a.detail-toggle.hide-details').present, - 'Certificate details are expanded' - ).fulfill() - - ################ - # Click Actions - ################ - - def click_create_certificate_button(self): - """ - Create a new certificate. - """ - disable_animations(self) - self.find_css('.action-primary').first.click() - self.wait_for_ajax() - - def click_save_certificate_button(self): - """ - Save certificate. - """ - self.find_css('.action-primary').first.click() - self.wait_for_ajax() - - def click_add_signatory_button(self): - """ - Add signatory to certificate - """ - self.find_css('.action-add-signatory').first.click() - - def click_edit_certificate_button(self): - """ - Open editing view for the certificate. - """ - self.find_css('.action-edit .edit').first.click() - - def click_cancel_edit_certificate(self): - """ - Cancel certificate editing. - """ - self.find_css('.action-secondary').first.click() - - def click_certificate_details_toggle(self): - """ - Expand/collapse certificate configuration. - """ - self.find_css('a.detail-toggle').first.click() - - def click_delete_certificate_button(self): - """ - Remove the first (possibly the only) certificate from the set - """ - self.wait_for_certificate_delete_button() - self.find_css('.actions .delete.action-icon').first.click() - - -class SignatorySectionPage(CertificatesPage): - """ - SignatorySectionPage is the signatory section within CertificatesSection, There might be multiple signatories - in a certificate section so this section object can be used to used to identify unique section and apply - operations on it. - """ - def __init__(self, container, prefix, mode, index): - """ - Initialize SignatorySection Page - - :param container: Container Section Page Object of the Signatory section - :param prefix: css selector of the container element - :param index: index of section in the signatory list on the page - :param mode: 'details' or 'edit', showing whether signatory is being displayed or edited - - :return: - """ - self.prefix = prefix - self.index = index - self.mode = mode - - super(SignatorySectionPage, self).__init__(container.browser, **container.course_info) - - def is_browser_on_page(self): - """ - Verify that the browser is on the page and it is not still loading. - """ - return self.q(css=self.prefix + " .signatory-details-list, .signatory-edit-list").present - - @property - def url(self): - """ - Construct a URL to the page section within the certificate section page. - """ - # This is a page section and can not be accessed directly - return None - - ################ - # Helpers - ################ - - @staticmethod - def file_path(filename): - """ - Construct file path to be uploaded from the data upload folder. - - Arguments: - filename (str): asset filename - - """ - # Should grab common point between this page module and the data folder. - return os.sep.join(os.path.abspath(__file__).split(os.sep)[:-4]) + '/data/uploads/' + filename - - def get_selector(self, css=''): - """ - Return selector fo signatory container - """ - selector = self.prefix + u' .signatory-{}-view-{}'.format(self.mode, self.index) - return ' '.join([selector, css]) - - def find_css(self, css_selector): - """ - Find elements as defined by css locator. - """ - return self.q(css=self.get_selector(css=css_selector)) - - ################ - # Properties - ################ - - @property - def name(self): - """ - Return signatory name. - """ - return self.find_css('.signatory-panel-body .signatory-name-value').first.text[0] - - @name.setter - def name(self, value): - """ - Set signatory name. - """ - self.find_css('.signatory-name-input').first.fill(value) - - @property - def title(self): - """ - Return signatory title. - """ - return self.find_css('.signatory-panel-body .signatory-title-value').first.text[0] - - @title.setter - def title(self, value): - """ - Set signatory title. - """ - self.find_css('.signatory-title-input').first.fill(value) - - @property - def organization(self): - """ - Return signatory organization. - """ - return self.find_css('.signatory-panel-body .signatory-organization-value').first.text[0] - - @organization.setter - def organization(self, value): - """ - Set signatory organization. - """ - self.find_css('.signatory-organization-input').first.fill(value) - - ################ - # Workflows - ################ - - def edit(self): - """ - Open editing view for the signatory. - """ - element = self.q(css='.edit-signatory').results[0] - mouse_hover_action = ActionChains(self.browser).move_to_element(element) - mouse_hover_action.perform() - self.wait_for_element_visibility('.edit-signatory', 'Edit button visibility') - element.click() - self.wait_for_signatory_edit_view() - - def delete_signatory(self): - """ - Delete the signatory - """ - self.wait_for_signatory_delete_icon() - self.click_signatory_delete_icon() - self.wait_for_signatory_delete_prompt() - - self.q(css='#prompt-warning a.button.action-primary').first.click() - self.wait_for_ajax() - - def save(self): - """ - Save signatory. - """ - # Click on the save button. - self.q(css='button.signatory-panel-save').click() - self.mode = 'details' - self.wait_for_ajax() - self.wait_for_signatory_detail_view() - - def close(self): - """ - Cancel signatory editing. - """ - self.q(css='button.signatory-panel-close').click() - self.mode = 'details' - self.wait_for_signatory_detail_view() - - def upload_signature_image(self, image_filename): - """ - Opens upload image dialog and upload given image file. - """ - self.wait_for_signature_image_upload_button() - self.find_css('.action-upload-signature').first.click() - self.wait_for_signature_image_upload_prompt() - - asset_file_path = self.file_path(image_filename) - self.q( - css='.assetupload-modal .upload-dialog input[type="file"]' - )[0].send_keys(asset_file_path) - - EmptyPromise( - lambda: not self.q( - css='.assetupload-modal a.action-upload.disabled' - ).present, - 'Upload button is not disabled anymore' - ).fulfill() - - self.q(css='.assetupload-modal a.action-upload').first.click() - EmptyPromise( - lambda: not self.q(css='.assetupload-modal .upload-dialog').visible, - 'Upload dialog is removed after uploading image' - ).fulfill() - - ################ - # Wait Actions - ################ - - @property - def wait_for_signatory_delete_icon(self): - """ - Returns whether or not the delete icon is present. - """ - EmptyPromise( - lambda: self.q(css='.signatory-panel-delete').present, - 'Delete icon is displayed' - ).fulfill() - - def wait_for_signatory_delete_prompt(self): - """ - Promise to wait until signatory delete prompt is visible - """ - EmptyPromise( - lambda: self.q(css='a.button.action-primary').present, - 'Delete prompt is displayed' - ).fulfill() - - def wait_for_signatory_edit_view(self): - """ - Promise to wait until signatory edit view is loaded - """ - EmptyPromise( - lambda: self.find_css('.signatory-panel-body .signatory-name-input').present, - 'On signatory edit view' - ).fulfill() - - def wait_for_signatory_detail_view(self): - """ - Promise to wait until signatory details view is loaded - """ - EmptyPromise( - lambda: self.find_css('.signatory-panel-body .signatory-name-value').present, - 'On signatory details view' - ).fulfill() - - def wait_for_signature_image_upload_prompt(self): - """ - Promise to wait until signatory image upload prompt is visible - """ - EmptyPromise( - lambda: self.q(css='.assetupload-modal .action-upload').present, - 'Signature image upload dialog opened' - ).fulfill() - - def wait_for_signature_image_upload_button(self): - """ - Promise to wait until signatory image upload button is visible - """ - EmptyPromise( - lambda: self.q(css=".action-upload-signature").first.present, - 'Signature image upload button available' - ).fulfill() - - @property - def wait_for_signature_image(self): - """ - Promise for the signature image to be displayed - """ - EmptyPromise( - lambda: self.q(css=".current-signature-image .signature-image").present, - 'Signature image available' - ).fulfill() - - ################ - # Click Actions - ################ - - def click_signatory_delete_icon(self): - """ - Clicks the signatory deletion icon/action - """ - self.find_css('.signatory-panel-delete').first.click() diff --git a/common/test/acceptance/pages/studio/settings_graders.py b/common/test/acceptance/pages/studio/settings_graders.py deleted file mode 100644 index 3d16926a57..0000000000 --- a/common/test/acceptance/pages/studio/settings_graders.py +++ /dev/null @@ -1,365 +0,0 @@ -""" -Course Grading Settings page. -""" - - -from bok_choy.javascript import requirejs -from bok_choy.promise import BrokenPromise -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.common.keys import Keys -from six.moves import range - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.studio.settings import SettingsPage -from common.test.acceptance.pages.studio.utils import press_the_notification_button - - -@requirejs('js/factories/settings_graders') -class GradingPage(SettingsPage): - """ - Course Grading Settings page. - """ - - url_path = "settings/grading" - grade_ranges = '.grades .grade-specific-bar' - grace_period_field = '#course-grading-graceperiod' - assignments = '.field-group.course-grading-assignment-list-item' - - 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_assignment_type(self): - """ - Click New Assignment Type button. - """ - self.q(css='.new-button.new-course-grading-item.add-grading-data').click() - - @property - def total_number_of_grades(self): - """ - Gets total number of grades present in the grades bar - Returns: - int: Single number length of grades - """ - self.wait_for_element_visibility(self.grade_ranges, 'Grades are visible') - return len(self.q(css=self.grade_ranges)) - - 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_grades(self, number_of_grades): - """ - Remove grade ranges from grades bar. - """ - for _ in range(number_of_grades): - self.browser.execute_script('document.getElementsByClassName("remove-button")[0].click()') - - def remove_all_grades(self): - """ - Removes all grades - """ - while len(self.q(css='.remove-button')) > 0: - self.remove_grade() - - def drag_and_drop_grade(self): - """ - Drag and drop grade range. - """ - self.wait_for_element_visibility(self.grade_ranges, "Grades ranges are visible") - action = ActionChains(self.browser) - moveable_css = self.q(css='.ui-resizable-e').results[0] - action.drag_and_drop_by_offset(moveable_css, -280, 0) - action.perform() - - @property - def get_assignment_names(self): - """ - Get name of the all the assignment types. - Returns: - list: A list containing names of the assignment types. - """ - self.wait_for_element_visibility( - '#course-grading-assignment-name', - 'Grade Names not visible.' - ) - return self.q(css='#course-grading-assignment-name').attrs('value') - - def change_assignment_name(self, old_name, new_name): - """ - Changes the assignment name. - Arguments: - old_name (str): The assignment type name which is to be changed. - new_name (str): New name of the assignment. - """ - self.wait_for_element_visibility('#course-grading-assignment-name', 'Assignment Name field visible') - self.q(css='#course-grading-assignment-name').filter( - lambda el: el.get_attribute('value') == old_name).fill(new_name) - - def set_weight(self, assignment_name, weight): - """ - Set the weight of the assignment type. - - Arguments: - assignment_name (string): Assignment name for which weight is to be changed. - weight (string): New weight - """ - weight_id = '#course-grading-assignment-gradeweight' - f = self.q(css=weight_id).results[-1] - for __ in range(len(assignment_name)): - f.send_keys(Keys.END, Keys.BACK_SPACE) - f.send_keys(weight) - - def get_assignment_weight(self, assignment_name): - """ - Gets the weight of assignment - - Arguments: - assignment_name (str): Name of the assignment - Returns: - string: Weight of the assignment - """ - self.wait_for_element_visibility( - '#course-grading-assignment-gradeweight', - 'Weight fields are present' - ) - weight_id = '#course-grading-assignment-gradeweight' - index = self._get_type_index(assignment_name) - all_weight_elements = self.q(css=weight_id).results - return all_weight_elements[index].get_attribute('value') - - def is_notification_button_disbaled(self): - """ - Check to see if notification button is disabled. - - Returns: - bool: True if button is disabled. - """ - self.wait_for_element_visibility('.nav-actions>ul', 'Notification bar not visible.') - return self.q(css='.action-primary.action-save.is-disabled').present - - def edit_grade_name(self, new_grade_name): - """ - Edit name of the highest grade. - """ - self.wait_for_element_visibility(self.grade_ranges, 'Grades are visible') - self.q(css='span[contenteditable="true"]').fill(new_grade_name) - - def try_edit_fail_grade(self, field_value): - """ - Try to edit the name of lowest grade. - """ - self.wait_for_element_visibility(self.grade_ranges, 'Grades are visible') - try: - self.q(css='span[contenteditable="false"]').fill(field_value) - except BrokenPromise: - pass - - @property - def highest_grade_name(self): - """ - Get name of the highest grade. - """ - self.wait_for_element_visibility(self.grade_ranges, 'Grades are visible') - return self.q(css='span[contenteditable="true"]').first.text[0] - - @property - def lowest_grade_name(self): - """ - Get name of the lowest grade. - """ - self.wait_for_element_visibility(self.grade_ranges, 'Grades are visible') - return self.q(css='span[contenteditable="false"]').first.text[0] - - @property - def grace_period_value(self): - """ - Get the grace period field value. - """ - self.wait_for( - lambda: self.q(css='#course-grading-graceperiod').attrs('value')[0] != '00:00', - description="Grace period field is updated after save" - ) - return self.q(css='#course-grading-graceperiod').attrs('value')[0] - - @property - def grade_letters(self): - """ - Get names of grade ranges. - - Returns: - list: A list containing names of the grade ranges. - """ - return self.q(css='.letter-grade').text - - def click_add_grade(self): - """ - Clicks to add a grade - """ - click_css(self, '.new-grade-button', require_notification=False) - - def is_grade_added(self, length): - """ - Checks to see if grade is added by comparing number of grades after the addition - - Returns: - bool: True if grade is added - bool: False if grade is not added - """ - try: - self.wait_for( - lambda: len(self.q(css=self.grade_ranges)) == length + 1, - description="Grades are added", - timeout=3 - ) - return True - except BrokenPromise: - return False - - @property - def grades_range(self): - """ - Get ranges of all the grades. - - Returns: - list: A list containing ranges of all the grades - """ - self.wait_for_element_visibility('.range', 'Ranges are visible') - return self.q(css='.range').text - - 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: - list: 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() - - @property - def confirmation_message(self): - """ - Get confirmation message received after saving settings. - """ - self.wait_for_element_visibility('#alert-confirmation-title', 'Confirmation text present') - return self.q(css='#alert-confirmation-title').text[0] - - def _get_type_index(self, name): - """ - Gets the index of assignment type. - - Arguments: - name(str): name of the assignment - - Returns: - int: index of the assignment type - """ - name_id = '#course-grading-assignment-name' - all_types = self.q(css=name_id).results - for index, element in enumerate(all_types): - if element.get_attribute('value') == name: - return index - return -1 - - def save(self): - """ - Click on save settings button. - """ - press_the_notification_button(self, "Save") - - def cancel(self): - """ - Click on cancel settings button. - """ - press_the_notification_button(self, "Cancel") - - def set_grace_period(self, grace_time_value): - """ - Set value in grace period field. - """ - self.set_element_value(grace_time_value) - - def check_field_value(self, field_value): - """ - Check updated values in input field - """ - self.wait_for( - lambda: self.q(css='#course-grading-graceperiod').attrs('value')[0] == field_value, - "Value of input field is correct." - ) - - def set_element_value(self, element_value): - """ - Set the values of the elements to those specified - in the element_values dict. - """ - element = self.q(css='#course-grading-graceperiod').results[0] - element.click() - element.clear() - self.wait_for( - lambda: self.q(css='#course-grading-graceperiod').attrs('value')[0] == '', - "Value of input field is correct." - ) - element.send_keys(element_value) diff --git a/common/test/acceptance/pages/studio/settings_group_configurations.py b/common/test/acceptance/pages/studio/settings_group_configurations.py deleted file mode 100644 index 29c916261e..0000000000 --- a/common/test/acceptance/pages/studio/settings_group_configurations.py +++ /dev/null @@ -1,358 +0,0 @@ -""" -Course Group Configurations page. -""" - - -from six.moves import range - -from common.test.acceptance.pages.common.utils import confirm_prompt -from common.test.acceptance.pages.studio.course_page import CoursePage - - -class GroupConfigurationsPage(CoursePage): - """ - Course Group Configurations page. - """ - - url_path = "group_configurations" - experiment_groups_css = ".experiment-groups" - content_groups_css = ".content-groups" - - def is_browser_on_page(self): - """ - Verify that the browser is on the page and it is not still loading. - """ - return all([ - self.q(css='body.view-group-configurations').present, - self.q(css='div.ui-loading.is-hidden').present - ]) - - @property - def experiment_group_configurations(self): - """ - Return list of the experiment group configurations for the course. - """ - return self._get_groups(self.experiment_groups_css) - - @property - def content_groups(self): - """ - Return list of the content groups for the course. - """ - return self._get_groups(self.content_groups_css) - - def _get_groups(self, prefix): - """ - Return list of the group-configurations-list-item's of specified type for the course. - """ - css = prefix + ' .wrapper-collection' - return [GroupConfiguration(self, prefix, index) for index in range(len(self.q(css=css)))] - - def create_experiment_group_configuration(self): - """ - Creates new group configuration. - """ - self.q(css=self.experiment_groups_css + " .new-button").first.click() - - def create_first_content_group(self): - """ - Creates new content group when there are none initially defined. - """ - self.q(css=self.content_groups_css + " .new-button").first.click() - - def add_content_group(self): - """ - Creates new content group when at least one already exists - """ - self.q(css=self.content_groups_css + " .action-add").first.click() - - @property - def no_experiment_groups_message_is_present(self): - return self._no_content_message(self.experiment_groups_css).present - - @property - def no_content_groups_message_is_present(self): - return self._no_content_message(self.content_groups_css).present - - @property - def no_experiment_groups_message_text(self): - return self._no_content_message(self.experiment_groups_css).text[0] - - @property - def no_content_groups_message_text(self): - return self._no_content_message(self.content_groups_css).text[0] - - def _no_content_message(self, prefix): - """ - Returns the message about "no content" for the specified type. - """ - return self.q(css='.wrapper-content ' + prefix + ' .no-content') - - @property - def experiment_group_sections_present(self): - """ - Returns whether or not anything related to content experiments is present. - """ - return self.q(css=self.experiment_groups_css).present or self.q(css=".experiment-groups-doc").present - - @property - def enrollment_track_section_present(self): - return self.q(css='.wrapper-groups.content-groups.enrollment_track').present - - @property - def enrollment_track_edit_present(self): - return self.q(css='.wrapper-groups.content-groups.enrollment_track .action.action-edit').present - - def get_enrollment_groups(self): - return self.q(css='.wrapper-groups.content-groups.enrollment_track .collection-details .title').text - - -class GroupConfiguration(object): - """ - Group Configuration wrapper. - """ - - def __init__(self, page, prefix, index): - self.page = page - self.SELECTOR = prefix + u' .wrapper-collection-{}'.format(index) - self.index = index - - def get_selector(self, css=''): - return ' '.join([self.SELECTOR, css]) - - def find_css(self, selector): - """ - Find elements as defined by css locator. - """ - return self.page.q(css=self.get_selector(css=selector)) - - def toggle(self): - """ - Expand/collapse group configuration. - """ - self.find_css('a.group-toggle').first.click() - - @property - def is_expanded(self): - """ - Group configuration usage information is expanded. - """ - return self.find_css('a.group-toggle.hide-groups').present - - def add_group(self): - """ - Add new group. - """ - self.find_css('button.action-add-group').first.click() - - def get_text(self, css): - """ - Return text for the defined by css locator. - """ - return self.find_css(css).first.text[0] - - def click_outline_anchor(self): - """ - Click on the `Course Outline` link. - """ - self.find_css('p.group-configuration-usage-text a').first.click() - - def click_unit_anchor(self, index=0): - """ - Click on the link to the unit. - """ - self.find_css('li.group-configuration-usage-unit a').nth(index).click() - - def edit(self): - """ - Open editing view for the group configuration. - """ - self.find_css('.action-edit .edit').first.click() - - @property - def delete_button_is_disabled(self): - return self.find_css('.actions .delete.is-disabled').present - - @property - def delete_button_is_present(self): - """ - Returns whether or not the delete icon is present. - """ - return self.find_css('.actions .delete').present - - def delete(self): - """ - Delete the group configuration. - """ - self.find_css('.actions .delete').first.click() - confirm_prompt(self.page) - - def save(self): - """ - Save group configuration. - """ - self.find_css('.action-primary').first.click() - self.page.wait_for_ajax() - - def cancel(self): - """ - Cancel group configuration. - """ - self.find_css('.action-secondary').first.click() - - @property - def mode(self): - """ - Return group configuration mode. - """ - if self.find_css('.collection-edit').present: - return 'edit' - elif self.find_css('.collection').present: - return 'details' - - @property - def id(self): - """ - Return group configuration id. - """ - return self.get_text('.group-configuration-id .group-configuration-value') - - @property - def validation_message(self): - """ - Return validation message. - """ - return self.get_text('.message-status.error') - - @property - def usages(self): - """ - Return list of usages. - """ - css = '.group-configuration-usage-unit' - return self.find_css(css).text - - @property - def name(self): - """ - Return group configuration name. - """ - return self.get_text('.title') - - @name.setter - def name(self, value): - """ - Set group configuration name. - """ - return self.find_css('.collection-name-input').first.fill(value) - - @property - def description(self): - """ - Return group configuration description. - """ - return self.get_text('.group-configuration-description') - - @description.setter - def description(self, value): - """ - Set group configuration description. - """ - self.find_css('.group-configuration-description-input').first.fill(value) - - @property - def groups(self): - """ - Return list of groups. - """ - def group_selector(group_index): - return self.get_selector('.group-{} '.format(group_index)) - - return [Group(self.page, group_selector(index)) for index, element in enumerate(self.find_css('.group'))] - - @property - def delete_note(self): - """ - Return delete note for the group configuration. - """ - return self.find_css('.wrapper-delete-button').first.attrs('data-tooltip')[0] - - @property - def details_error_icon_is_present(self): - return self.find_css('.wrapper-group-configuration-usages .fa-exclamation-circle').present - - @property - def details_warning_icon_is_present(self): - return self.find_css('.wrapper-group-configuration-usages .fa-warning').present - - @property - def details_message_is_present(self): - return self.find_css('.wrapper-group-configuration-usages .group-configuration-validation-message').present - - @property - def details_message_text(self): - return self.find_css('.wrapper-group-configuration-usages .group-configuration-validation-message').text[0] - - @property - def edit_warning_icon_is_present(self): - return self.find_css('.wrapper-group-configuration-validation .fa-warning').present - - @property - def edit_warning_message_is_present(self): - return self.find_css('.wrapper-group-configuration-validation .group-configuration-validation-text').present - - @property - def edit_warning_message_text(self): - return self.find_css('.wrapper-group-configuration-validation .group-configuration-validation-text').text[0] - - def __repr__(self): - return "<{}:{}>".format(self.__class__.__name__, self.name) - - -class Group(object): - """ - Group wrapper. - """ - def __init__(self, page, prefix_selector): - self.page = page - self.prefix = prefix_selector - - def find_css(self, selector): - """ - Find elements as defined by css locator. - """ - return self.page.q(css=self.prefix + selector) - - @property - def name(self): - """ - Return the name of the group . - """ - css = '.group-name' - return self.find_css(css).first.text[0] - - @name.setter - def name(self, value): - """ - Set the name for the group. - """ - css = '.group-name' - self.find_css(css).first.fill(value) - - @property - def allocation(self): - """ - Return allocation for the group. - """ - css = '.group-allocation' - return self.find_css(css).first.text[0] - - def remove(self): - """ - Remove the group. - """ - css = '.action-close' - return self.find_css(css).first.click() - - def __repr__(self): - return "<{}:{}>".format(self.__class__.__name__, self.name) diff --git a/common/test/acceptance/pages/studio/signup.py b/common/test/acceptance/pages/studio/signup.py deleted file mode 100644 index 3e9f2d944e..0000000000 --- a/common/test/acceptance/pages/studio/signup.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Signup page for studio -""" - - -from bok_choy.page_object import PageObject - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.studio import LMS_URL -from common.test.acceptance.pages.studio.utils import HelpMixin, set_input_value - - -class SignupPage(PageObject, HelpMixin): - """ - Signup page for Studio. - """ - - url = LMS_URL + "/register" - - def is_browser_on_page(self): - return ( - self.q(css="#register-anchor").is_present() and - self.q(css=".register-button").visible - ) - - def input_password(self, password): - """Inputs a password and then returns the password input""" - return set_input_value(self, "#register-password", password) - - def sign_up_user(self, email, name, username, password, country="US", favorite_movie="Alf"): - """ - Register the user. - """ - self.q(css="#register-email").fill(email) - self.q(css="#register-name").fill(name) - self.q(css="#register-username").fill(username) - self.q(css="#register-password").fill(password) - self.q(css="#register-country").results[0].send_keys(country) - self.q(css="#register-favorite_movie").fill(favorite_movie) - - # Submit it - self.q(css=".register-button").click() - self.wait_for_element_absence('.register-button', 'Register button is gone.') diff --git a/common/test/acceptance/pages/studio/textbook_upload.py b/common/test/acceptance/pages/studio/textbook_upload.py deleted file mode 100644 index c730e8b3b8..0000000000 --- a/common/test/acceptance/pages/studio/textbook_upload.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -Course Textbooks page. -""" - - -import requests -from path import Path as path - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.studio.course_page import CoursePage - - -class TextbookUploadPage(CoursePage): - """ - Course Textbooks page. - """ - - url_path = "textbooks" - - def is_browser_on_page(self): - return self.q(css='.textbooks-list').visible - - def open_add_textbook_form(self): - """ - Open new textbook form by clicking on new textbook button. - """ - self.q(css='.nav-item .new-button').click() - - def get_element_text(self, selector): - """ - Return the text of the css selector. - """ - return self.q(css=selector)[0].text - - def set_input_field_value(self, selector, value): - """ - Set the value of input field by selector. - """ - self.q(css=selector)[0].send_keys(value) - - def upload_pdf_file(self, file_name): - """ - Uploads a pdf textbook. - """ - # If the pdf upload section has not yet been toggled on, click on the upload pdf button - test_dir = path(__file__).abspath().dirname().dirname().dirname().dirname() - file_path = test_dir + '/data/uploads/' + file_name - - click_css(self, ".edit-textbook .action-upload", require_notification=False) - self.wait_for_element_visibility(".upload-dialog input", "Upload modal opened") - file_input = self.q(css=".upload-dialog input").results[0] - file_input.send_keys(file_path) - click_css(self, ".wrapper-modal-window-assetupload .action-upload", require_notification=False) - self.wait_for_element_absence(".modal-window-overlay", "Upload modal closed") - - def click_textbook_submit_button(self): - """ - Submit the new textbook form and check if it is rendered properly. - """ - self.wait_for_element_visibility('#edit_textbook_form button[type="submit"]', 'Save button visibility') - self.q(css='#edit_textbook_form button[type="submit"]').first.click() - self.wait_for_element_absence(".wrapper-form", "Add/Edit form closed") - - def is_view_live_link_worked(self): - """ - Check if the view live button of textbook is working fine. - """ - try: - self.wait_for(lambda: len(self.q(css='.textbook a.view').attrs('href')) > 0, "href value present") - response = requests.get(self.q(css='.textbook a.view').attrs('href')[0]) - except requests.exceptions.ConnectionError: - return False - - return response.status_code == 200 - - def upload_new_textbook(self): - """ - Fills out form to upload a new textbook - """ - self.open_add_textbook_form() - self.upload_pdf_file('textbook.pdf') - self.set_input_field_value('.edit-textbook #textbook-name-input', 'book_1') - self.set_input_field_value('.edit-textbook #chapter1-name', 'chap_1') - self.click_textbook_submit_button() - - def set_textbook_name(self, textbook_name): - """ - Set the name of textbook. - """ - self.open_add_textbook_form() - self.set_input_field_value('.edit-textbook #textbook-name-input', textbook_name) - - def fill_chapter_name(self, ordinal, chapter_name): - """ - Adds chapter name by taking the ordinal of the chapter. - """ - index = ["first", "second", "third"].index(ordinal) - self.set_input_field_value(u'.textbook .chapter{i} input.chapter-name'.format(i=index + 1), chapter_name) - - def fill_chapter_asset(self, ordinal, chapter_asset): - """ - Adds chapter asset by taking the ordinal of the chapter. - """ - index = ["first", "second", "third"].index(ordinal) - self.set_input_field_value(u'.textbook .chapter{i} input.chapter-asset-path'.format(i=index + 1), chapter_asset) - - def submit_chapter(self): - """ - Click on Add Chapter button. - """ - self.q(css='.action.action-add-chapter').first.click() - - @property - def textbook_name(self): - """ - Gets the name of a saved textbook. - """ - return self.q(css='.textbook-title').text[0] - - def get_chapter_name(self, index): - """ - Gets the name of chapter by taking an ordinal. - """ - return self.q(css='.chapter-name').text[index] - - def get_asset_name(self, index): - """ - Gets the name of chapter asset by taking an ordinal. - """ - return self.q(css='.chapter-asset-path').text[index] - - def toggle_chapters(self): - """ - Toggle saved chapters. - """ - self.q(css='.chapter-toggle.show-chapters').click() - - @property - def number_of_chapters(self): - """ - Gets the total number of saved chapters. - """ - return len(self.q(css='.chapter').results) - - def refresh_and_wait_for_load(self): - """ - Refresh the page and wait for all resources to load. - """ - self.browser.refresh() - self.wait_for_page() diff --git a/common/test/acceptance/pages/studio/users.py b/common/test/acceptance/pages/studio/users.py index d68156944b..cca2cf2329 100644 --- a/common/test/acceptance/pages/studio/users.py +++ b/common/test/acceptance/pages/studio/users.py @@ -2,17 +2,9 @@ Page classes to test either the Course Team page or the Library Team page. """ - -import os - -import six from bok_choy.page_object import PageObject from bok_choy.promise import EmptyPromise -from opaque_keys.edx.locator import CourseLocator -from common.test.acceptance.pages.studio import BASE_URL -from common.test.acceptance.pages.studio.course_page import CoursePage -from common.test.acceptance.pages.studio.utils import HelpMixin from common.test.acceptance.tests.helpers import disable_animations @@ -39,103 +31,6 @@ class UsersPageMixin(PageObject): """ raise NotImplementedError - def is_browser_on_page(self): - """ - Returns True if the browser has loaded the page. - """ - return self.q(css='body.view-team').present and not self.q(css='.ui-loading').present - - @property - def users(self): - """ - Return a list of users listed on this page. - """ - return self.q(css='.user-list .user-item').map( - lambda el: UserWrapper(self.browser, el.get_attribute('data-email')) - ).results - - @property - def usernames(self): - """ - Returns a list of user names for users listed on this page - """ - return [user.name for user in self.users] - - @property - def has_add_button(self): - """ - Is the "New Team Member" button present? - """ - return self.q(css='.create-user-button').present - - def click_add_button(self): - """ - Click on the "New Team Member" button - """ - self.q(css='.create-user-button').first.click() - self.wait_for(lambda: self.new_user_form_visible, "Add user form is visible") - - @property - def new_user_form_visible(self): - """ Is the new user form visible? """ - return self.q(css='.form-create.create-user .user-email-input').visible - - def set_new_user_email(self, email): - """ Set the value of the "New User Email Address" field. """ - self.q(css='.form-create.create-user .user-email-input').fill(email) - - def click_submit_new_user_form(self): - """ Submit the "New User" form """ - self.q(css='.form-create.create-user .action-primary').click() - wait_for_ajax_or_reload(self.browser) - self.wait_for_element_visibility('.user-list', 'wait for team to load') - - def get_user(self, email): - """ Gets user wrapper by email """ - target_users = [user for user in self.users if user.email == email] - assert len(target_users) == 1 - return target_users[0] - - def add_user_to_course(self, email): - """ Adds user to a course/library """ - self.wait_for_element_visibility('.create-user-button', "Add team member button is available") - self.click_add_button() - self.set_new_user_email(email) - self.click_submit_new_user_form() - self.wait_for_page() - - def delete_user_from_course(self, email): - """ Deletes user from course/library """ - target_user = self.get_user(email) - target_user.click_delete() - self.wait_for_page() - - def modal_dialog_visible(self, dialog_type): - """ Checks if modal dialog of specified class is displayed """ - return self.q(css='.prompt.{dialog_type}'.format(dialog_type=dialog_type)).visible - - def modal_dialog_text(self, dialog_type): - """ Gets modal dialog text """ - return self.q(css=u'.prompt.{dialog_type} .message'.format(dialog_type=dialog_type)).text[0] - - def wait_until_no_loading_indicator(self): - """ - When the page first loads, there is a loading indicator and most - functionality is not yet available. This waits for that loading to finish - and be removed from the DOM. - - This method is different from wait_until_ready because the loading element - is removed from the DOM, rather than hidden. - - It also disables animations for improved test reliability. - """ - - self.wait_for( - lambda: not self.q(css='.ui-loading').present, - "Wait for page to complete its initial loading" - ) - disable_animations(self) - def wait_until_ready(self): """ When the page first loads, there is a loading indicator and most @@ -153,145 +48,3 @@ class UsersPageMixin(PageObject): 'Wait for the page to complete its initial loading' ) disable_animations(self) - - -class LibraryUsersPage(UsersPageMixin, HelpMixin): - """ - Library Team page in Studio - """ - def __init__(self, browser, locator): - super(LibraryUsersPage, self).__init__(browser) - self.locator = locator - - @property - def url(self): - """ - URL to the "User Access" page for the given library. - """ - return "{}/library/{}/team/".format(BASE_URL, six.text_type(self.locator)) - - -class CourseTeamPage(UsersPageMixin, CoursePage): - """ - Course Team page in Studio. - """ - url_path = "course_team" - - @property - def url(self): - """ - Construct a URL to the page within the course. - """ - # TODO - is there a better way to make this agnostic to the underlying default module store? - default_store = os.environ.get('DEFAULT_STORE', 'draft') - course_key = CourseLocator( - self.course_info['course_org'], - self.course_info['course_num'], - self.course_info['course_run'], - deprecated=(default_store == 'draft') - ) - return "/".join([BASE_URL, self.url_path, six.text_type(course_key)]) - - -class UserWrapper(PageObject): - """ - A PageObject representing a wrapper around a user listed on the course/library team page. - """ - url = None - COMPONENT_BUTTONS = { - 'basic_tab': '.editor-tabs li.inner_tab_wrap:nth-child(1) > a', - 'advanced_tab': '.editor-tabs li.inner_tab_wrap:nth-child(2) > a', - 'save_settings': '.action-save', - } - - def __init__(self, browser, email): - super(UserWrapper, self).__init__(browser) - self.email = email - self.selector = u'.user-list .user-item[data-email="{}"]'.format(self.email) - - def is_browser_on_page(self): - """ - Sanity check that our wrapper element is on the page. - """ - return self.q(css=self.selector).present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to this particular user entry's context - """ - return u'{} {}'.format(self.selector, selector) - - @property - def name(self): - """ Get this user's username, as displayed. """ - text = self.q(css=self._bounded_selector('.user-username')).text - return text[0] if text else None - - @property - def role_label(self): - """ Get this user's role, as displayed. """ - text = self.q(css=self._bounded_selector('.flag-role .value')).text - return text[0] if text else None - - @property - def is_current_user(self): - """ Does the UI indicate that this is the current user? """ - return self.q(css=self._bounded_selector('.flag-role .msg-you')).present - - @property - def can_promote(self): - """ Can this user be promoted to a more powerful role? """ - return self.q(css=self._bounded_selector('.add-admin-role')).present - - @property - def promote_button_text(self): - """ What does the promote user button say? """ - text = self.q(css=self._bounded_selector('.add-admin-role')).text - return text[0] if text else None - - def click_promote(self): - """ Click on the button to promote this user to the more powerful role """ - self.q(css=self._bounded_selector('.add-admin-role')).click() - wait_for_ajax_or_reload(self.browser) - - @property - def can_demote(self): - """ Can this user be demoted to a less powerful role? """ - return self.q(css=self._bounded_selector('.remove-admin-role')).present - - @property - def demote_button_text(self): - """ What does the demote user button say? """ - text = self.q(css=self._bounded_selector('.remove-admin-role')).text - return text[0] if text else None - - def click_demote(self): - """ Click on the button to demote this user to the less powerful role """ - self.q(css=self._bounded_selector('.remove-admin-role')).click() - wait_for_ajax_or_reload(self.browser) - - @property - def can_delete(self): - """ Can this user be deleted? """ - return self.q(css=self._bounded_selector('.action-delete:not(.is-disabled) .remove-user')).present - - def click_delete(self): - """ Click the button to delete this user. """ - disable_animations(self) - self.q(css=self._bounded_selector('.remove-user')).click() - # We can't use confirm_prompt because its wait_for_ajax is flaky when the page is expected to reload. - self.wait_for_element_visibility('.prompt', 'Prompt is visible') - self.wait_for_element_visibility('.prompt .action-primary', 'Confirmation button is visible') - self.q(css='.prompt .action-primary').click() - self.wait_for_element_absence('.page-prompt .is-shown', 'Confirmation prompt is hidden') - wait_for_ajax_or_reload(self.browser) - - @property - def has_no_change_warning(self): - """ Does this have a warning in place of the promote/demote buttons? """ - return self.q(css=self._bounded_selector('.notoggleforyou')).present - - @property - def no_change_warning_text(self): - """ Text of the warning seen in place of the promote/demote buttons. """ - return self.q(css=self._bounded_selector('.notoggleforyou')).text[0] diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py index c82dd0189a..1da7868054 100644 --- a/common/test/acceptance/pages/studio/utils.py +++ b/common/test/acceptance/pages/studio/utils.py @@ -4,11 +4,8 @@ Utility methods useful for Studio page tests. from bok_choy.javascript import js_defined -from bok_choy.promise import EmptyPromise -from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys -from common.test.acceptance.pages.common.utils import click_css, sync_on_notification from common.test.acceptance.tests.helpers import click_and_wait_for_window NAV_HELP_NOT_SIGNED_IN_CSS = '.nav-item.nav-not-signedin-help a' @@ -17,138 +14,6 @@ SIDE_BAR_HELP_AS_LIST_ITEM = '.bit li.action-item a' SIDE_BAR_HELP_CSS = '.external-help a, .external-help-button' -@js_defined('window.jQuery') -def press_the_notification_button(page, name): - # Because the notification uses a CSS transition, - # Selenium will always report it as being visible. - # This makes it very difficult to successfully click - # the "Save" button at the UI level. - # Instead, we use JavaScript to reliably click - # the button. - btn_css = u'div#page-notification button.action-%s' % name.lower() - page.browser.execute_script(u"$('{}').focus().click()".format(btn_css)) - page.wait_for_ajax() - - -def add_discussion(page, menu_index=0): - """ - Add a new instance of the discussion category. - - menu_index specifies which instance of the menus should be used (based on vertical - placement within the page). - """ - page.wait_for_component_menu() - click_css(page, 'button>span.large-discussion-icon', menu_index) - - -def add_advanced_component(page, menu_index, name): - """ - Adds an instance of the advanced component with the specified name. - - menu_index specifies which instance of the menus should be used (based on vertical - placement within the page). - """ - # Click on the Advanced icon. - page.wait_for_component_menu() - click_css(page, 'button>span.large-advanced-icon', menu_index, require_notification=False) - - # This does an animation to hide the first level of buttons - # and instead show the Advanced buttons that are available. - # We should be OK though because click_css turns off jQuery animations - - # Make sure that the menu of advanced components is visible before clicking (the HTML is always on the - # page, but will have display none until the large-advanced-icon is clicked). - page.wait_for_element_visibility('.new-component-advanced', 'Advanced component menu is visible') - - # Now click on the component to add it. - component_css = u'button[data-category={}]'.format(name) - page.wait_for_element_visibility(component_css, u'Advanced component {} is visible'.format(name)) - - # Adding some components, e.g. the Discussion component, will make an ajax call - # but we should be OK because the click_css method is written to handle that. - click_css(page, component_css, 0) - - -def add_component(page, item_type, specific_type, is_advanced_problem=False): - """ - Click one of the "Add New Component" buttons. - - item_type should be "advanced", "html", "problem", or "video" - - specific_type is required for some types and should be something like - "Blank Common Problem". - """ - btn = page.q(css=u'.add-xblock-component .add-xblock-component-button[data-type={}]'.format(item_type)) - multiple_templates = btn.filter(lambda el: 'multiple-templates' in el.get_attribute('class')).present - btn.click() - if multiple_templates: - sub_template_menu_div_selector = u'.new-component-{}'.format(item_type) - page.wait_for_element_visibility(sub_template_menu_div_selector, 'Wait for the templates sub-menu to appear') - page.wait_for_element_invisibility( - '.add-xblock-component .new-component', - 'Wait for the add component menu to disappear' - ) - - # "Common Problem Types" are shown by default. - # For advanced problem types you must first select the "Advanced" tab. - if is_advanced_problem: - advanced_tab = page.q(css='.problem-type-tabs a').filter(text='Advanced').first - advanced_tab.click() - - # Wait for the advanced tab to be active - css = '.problem-type-tabs li.ui-tabs-active a' - page.wait_for( - lambda: len(page.q(css=css).filter(text=u'Advanced').execute()) > 0, - 'Waiting for the Advanced problem tab to be active' - ) - - all_options = page.q(css=u'.new-component-{} ul.new-component-template li button span'.format(item_type)) - chosen_option = all_options.filter(text=specific_type).first - chosen_option.click() - sync_on_notification(page) - page.wait_for_ajax() - - -def add_components(page, item_type, items, is_advanced_problem=False): - """ - Adds multiple components of a specific type. - item_type should be "advanced", "html", "problem", or "video" - items is a list of components of specific type to be added. - Please note that if you want to create an advanced problem - then all other items must be of advanced problem type. - """ - for item in items: - add_component(page, item_type, item, is_advanced_problem) - - -def add_html_component(page, menu_index, boilerplate=None): - """ - Adds an instance of the HTML component with the specified name. - - menu_index specifies which instance of the menus should be used (based on vertical - placement within the page). - """ - # Click on the HTML icon. - page.wait_for_component_menu() - click_css(page, 'button>span.large-html-icon', menu_index, require_notification=False) - - # Make sure that the menu of HTML components is visible before clicking - page.wait_for_element_visibility('.new-component-html', 'HTML component menu is visible') - - # Now click on the component to add it. - component_css = u'button[data-category=html]' - if boilerplate: - component_css += u'[data-boilerplate={}]'.format(boilerplate) - else: - component_css += u':not([data-boilerplate])' - - page.wait_for_element_visibility(component_css, u'HTML component {} is visible'.format(boilerplate)) - - # Adding some components will make an ajax call but we should be OK because - # the click_css method is written to handle that. - click_css(page, component_css, 0) - - @js_defined('window.jQuery') def type_in_codemirror(page, index, text, find_prefix="$"): script = u""" @@ -169,17 +34,6 @@ def get_codemirror_value(page, index=0, find_prefix="$"): ) -def get_input_value(page, css_selector): - """ - Returns the value of the field matching the css selector. - """ - page.wait_for_element_presence( - css_selector, - u'Elements matching "{}" selector are present'.format(css_selector) - ) - return page.q(css=css_selector).attrs('value')[0] - - def set_input_value(page, css, value): """ Sets the text field with the given label (display name) to the specified value. @@ -202,28 +56,6 @@ def set_input_value_and_save(page, css, value): page.wait_for_ajax() -def drag(page, source_index, target_index, placeholder_height=0): - """ - Gets the drag handle with index source_index (relative to the vertical layout of the page) - and drags it to the location of the drag handle with target_index. - - This should drag the element with the source_index drag handle BEFORE the - one with the target_index drag handle. - """ - draggables = page.q(css='.drag-handle') - source = draggables[source_index] - target = draggables[target_index] - action = ActionChains(page.browser) - action.click_and_hold(source).move_to_element_with_offset( - target, 0, placeholder_height - ) - if placeholder_height == 0: - action.release(target).perform() - else: - action.release().perform() - sync_on_notification(page) - - def verify_ordering(test_class, page, expected_orderings): """ Verifies the expected ordering of xblocks on the page. @@ -247,27 +79,6 @@ def verify_ordering(test_class, page, expected_orderings): test_class.assertEqual(len(blocks_checked), len(xblocks)) -def click_studio_help(page): - """ - Click the Studio help link in the page footer. - """ - help_link_selector = '.cta-show-sock' - # check if help link is visible - EmptyPromise(lambda: page.q(css=help_link_selector).visible, "Help link visible").fulfill() - - page.q(css=help_link_selector).click() - - # check if extended support section is visible. - EmptyPromise( - lambda: page.q(css='.support .list-actions a').results[0].text != '', 'Support section opened' - ).fulfill() - - -def studio_help_links(page): - """Return the list of Studio help links in the page footer.""" - return page.q(css='.support .list-actions a').results - - class HelpMixin(object): """ Mixin for testing Help links. diff --git a/common/test/acceptance/pages/studio/xblock_editor.py b/common/test/acceptance/pages/studio/xblock_editor.py deleted file mode 100644 index 2abad0aac2..0000000000 --- a/common/test/acceptance/pages/studio/xblock_editor.py +++ /dev/null @@ -1,197 +0,0 @@ -""" -Acceptance test xblock-editor. -""" - - -from bok_choy.page_object import PageObject -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.ui import Select - -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.tests.helpers import get_selected_option_text, select_option_by_text - - -class BaseXBlockEditorView(PageObject): - """ - A base :class:`.PageObject` for the xblock and visibility editors. - - This class assumes that the editor is our default editor as displayed for xmodules. - """ - BODY_SELECTOR = '.xblock-editor' - - def __init__(self, browser, locator): - """ - Args: - browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in. - locator (str): The locator that identifies which xblock this :class:`.xblock-editor` relates to. - """ - super(BaseXBlockEditorView, self).__init__(browser) - self.locator = locator - - def is_browser_on_page(self): - return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present - - def _bounded_selector(self, selector): - """ - Return `selector`, but limited to this particular `XBlockEditorView` context - """ - return u'{}[data-locator="{}"] {}'.format( - self.BODY_SELECTOR, - self.locator, - selector - ) - - def url(self): - """ - Returns None because this is not directly accessible via URL. - """ - return None - - def save(self): - """ - Clicks save button. - """ - click_css(self, 'a.action-save') - - def cancel(self): - """ - Clicks cancel button. - """ - click_css(self, 'a.action-cancel', require_notification=False) - - -class XBlockEditorView(BaseXBlockEditorView): - """ - A :class:`.PageObject` representing the rendered view of an xblock editor. - """ - def get_setting_element(self, label): - """ - Returns the index of the setting entry with given label (display name) within the Settings modal. - """ - settings_button = self.q(css='.edit-xblock-modal .editor-modes .settings-button') - if settings_button.is_present(): - settings_button.click() - setting_labels = self.q(css=self._bounded_selector('.metadata_edit .wrapper-comp-setting .setting-label')) - for index, setting in enumerate(setting_labels): - if setting.text == label: - return self.q(css=self._bounded_selector('.metadata_edit div.wrapper-comp-setting .setting-input'))[index] - return None - - def set_field_value_and_save(self, label, value): - """ - Sets the text field with given label (display name) to the specified value, and presses Save. - """ - elem = self.get_setting_element(label) - - # Clear the current value, set the new one, then - # Tab to move to the next field (so change event is triggered). - elem.clear() - elem.send_keys(value) - elem.send_keys(Keys.TAB) - self.save() - - def set_select_value_and_save(self, label, value): - """ - Sets the select with given label (display name) to the specified value, and presses Save. - """ - elem = self.get_setting_element(label) - select = Select(elem) - select.select_by_value(value) - self.save() - - def get_selected_option_text(self, label): - """ - Returns the text of the first selected option for the select with given label (display name). - """ - elem = self.get_setting_element(label) - if elem: - select = Select(elem) - return select.first_selected_option.text - else: - return None - - -class XBlockVisibilityEditorView(BaseXBlockEditorView): - """ - A :class:`.PageObject` representing the rendered view of an xblock visibility editor. - """ - OPTION_SELECTOR = '.partition-group-control .field' - ALL_LEARNERS_AND_STAFF = 'All Learners and Staff' - CONTENT_GROUP_PARTITION = 'Content Groups' - ENROLLMENT_TRACK_PARTITION = "Enrollment Track Groups" - - @property - def all_group_options(self): - """ - Return all partition groups. - """ - return self.q(css=self._bounded_selector(self.OPTION_SELECTOR)).results - - @property - def current_groups_message(self): - """ - This returns the message shown at the top of the visibility dialog about the - current visibility state (at the time that the dialog was opened). - For example, "Access is restricted to: All Learners and Staff". - """ - return self.q(css=self._bounded_selector('.visibility-header'))[0].text - - @property - def selected_partition_scheme(self): - """ - Return the selected partition scheme (or "All Learners and Staff" - if no partitioning is selected). - """ - selector = self.q(css=self._bounded_selector('.partition-visibility select')) - return get_selected_option_text(selector) - - def select_partition_scheme(self, partition_name): - """ - Sets the selected partition scheme to the one with the - matching name. - """ - selector = self.q(css=self._bounded_selector('.partition-visibility select')) - select_option_by_text(selector, partition_name, focus_out=True) - - @property - def selected_groups(self): - """ - Return all selected partition groups. If none are selected, - returns an empty array. - """ - results = [] - for option in self.all_group_options: - checkbox = option.find_element_by_css_selector('input') - if checkbox.is_selected(): - results.append(option) - return results - - def select_group(self, group_name, save=True): - """ - Select the first group which has a label matching `group_name`. - - Arguments: - group_name (str): The name of the group. - save (boolean): Whether the "save" button should be clicked - afterwards. - Returns: - bool: Whether a group with the provided name was found and clicked. - """ - for option in self.all_group_options: - if group_name in option.text: - checkbox = option.find_element_by_css_selector('input') - checkbox.click() - if save: - self.save() - return True - return False - - def select_groups_in_partition_scheme(self, partition_name, group_names): - """ - Select groups in the provided partition scheme. The "save" - button is clicked afterwards. - """ - self.select_partition_scheme(partition_name) - for label in group_names: - self.select_group(label, save=False) - self.save() diff --git a/common/test/acceptance/pages/xblock/__init__.py b/common/test/acceptance/pages/xblock/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/test/acceptance/pages/xblock/acid.py b/common/test/acceptance/pages/xblock/acid.py deleted file mode 100644 index 31730fcdfc..0000000000 --- a/common/test/acceptance/pages/xblock/acid.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -PageObjects related to the AcidBlock -""" - - -from bok_choy.page_object import PageObject -from bok_choy.promise import BrokenPromise, EmptyPromise - -from common.test.acceptance.pages.xblock.utils import wait_for_xblock_initialization - - -class AcidView(PageObject): - """ - A :class:`.PageObject` representing the rendered view of the :class:`.AcidBlock`. - """ - url = None - - def __init__(self, browser, context_selector): - """ - Args: - browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in. - context_selector (str): The selector that identifies where this :class:`.AcidBlock` view - is on the page. - """ - super(AcidView, self).__init__(browser) - self.context_selector = context_selector - - def is_browser_on_page(self): - - # First make sure that an element with the view-container class is present on the page, - # and then wait to make sure that the xblock has finished initializing. - return ( - self.q(css=u'{} .acid-block'.format(self.context_selector)).present and - wait_for_xblock_initialization(self, self.context_selector) and - self._ajax_finished() - ) - - def _ajax_finished(self): - try: - EmptyPromise( - lambda: self.browser.execute_script("return jQuery.active") == 0, - "AcidBlock tests still running", - timeout=240 - ).fulfill() - except BrokenPromise: - return False - else: - return True - - def test_passed(self, test_selector): - """ - Return whether a particular :class:`.AcidBlock` test passed. - """ - selector = u'{} .acid-block {} .pass'.format(self.context_selector, test_selector) - return bool(self.q(css=selector).results) - - def child_test_passed(self, test_selector): - """ - Return whether a particular :class:`.AcidParentBlock` test passed. - """ - selector = u'{} .acid-parent-block {} .pass'.format(self.context_selector, test_selector) - return bool(self.q(css=selector).execute(try_interval=0.1, timeout=3)) - - @property - def init_fn_passed(self): - """ - Whether the init-fn test passed in this view of the :class:`.AcidBlock`. - """ - return self.test_passed('.js-init-run') - - @property - def child_tests_passed(self): - """ - Whether the tests of children passed - """ - return all([ - self.child_test_passed('.child-counts-match'), - self.child_test_passed('.child-values-match') - ]) - - @property - def resource_url_passed(self): - """ - Whether the resource-url test passed in this view of the :class:`.AcidBlock`. - """ - return self.test_passed('.local-resource-test') - - def scope_passed(self, scope): - return all( - self.test_passed(u'.scope-storage-test.scope-{} {}'.format(scope, test)) - for test in ( - ".server-storage-test-returned", - ".server-storage-test-succeeded", - ".client-storage-test-returned", - ".client-storage-test-succeeded", - ) - ) - - def __repr__(self): - return "{}(, {!r})".format(self.__class__.__name__, self.context_selector) diff --git a/common/test/acceptance/pages/xblock/utils.py b/common/test/acceptance/pages/xblock/utils.py deleted file mode 100644 index 02f59eb914..0000000000 --- a/common/test/acceptance/pages/xblock/utils.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Utility methods useful for XBlock page tests. -""" - - -from bok_choy.promise import Promise - - -def wait_for_xblock_initialization(page, xblock_css): - """ - Wait for the xblock with the given CSS to finish initializing. - """ - def _is_finished_loading(): - # Wait for the xblock javascript to finish initializing - is_done = page.browser.execute_script(u"return $('{}').data('initialized')".format(xblock_css)) - return is_done, is_done - - return Promise(_is_finished_loading, 'Finished initializing the xblock.').fulfill() diff --git a/common/test/acceptance/setup.py b/common/test/acceptance/setup.py deleted file mode 100644 index 711a9f9d35..0000000000 --- a/common/test/acceptance/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python - -""" -Install bok-choy page objects for acceptance and end-to-end tests. -""" - - -import os - -from setuptools import setup - -VERSION = '0.0.1' -DESCRIPTION = "Bok-choy page objects for edx-platform" - -# Pip 1.5 will try to install this package from outside -# the directory containing setup.py, so we need to use an absolute path. -ACCEPTANCE_PACKAGE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__))) - -setup( - name='edxapp-acceptance', - version=VERSION, - author='edX', - url='http://github.com/edx/edx-platform', - description=DESCRIPTION, - license='AGPL', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Quality Assurance' - ], - package_dir={'edxapp_acceptance': ACCEPTANCE_PACKAGE_DIR}, - packages=['edxapp_acceptance', - 'edxapp_acceptance.pages', - 'edxapp_acceptance.pages.lms', - 'edxapp_acceptance.pages.studio', - 'edxapp_acceptance.pages.common', - 'edxapp_acceptance.tests'] -) diff --git a/common/test/acceptance/tests/discussion/helpers.py b/common/test/acceptance/tests/discussion/helpers.py index 9557af6ed0..b0d562ecd5 100644 --- a/common/test/acceptance/tests/discussion/helpers.py +++ b/common/test/acceptance/tests/discussion/helpers.py @@ -13,9 +13,7 @@ from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureD from common.test.acceptance.fixtures.discussion import ( ForumsConfigMixin, MultipleThreadFixture, - Response, - SingleThreadViewFixture, - Thread + Thread, ) from common.test.acceptance.pages.lms.discussion import DiscussionTabSingleThreadPage from common.test.acceptance.tests.helpers import UniqueCourseTest @@ -25,22 +23,6 @@ class BaseDiscussionMixin(object): """ A mixin containing methods common to discussion tests. """ - def setup_thread(self, num_responses, **thread_kwargs): - """ - Create a test thread with the given number of responses, passing all - keyword arguments through to the Thread fixture, then invoke - setup_thread_page. - """ - thread_id = "test_thread_{}".format(uuid4().hex) - thread_fixture = SingleThreadViewFixture( - Thread(id=thread_id, commentable_id=self.discussion_id, **thread_kwargs) - ) - for i in range(num_responses): - thread_fixture.addResponse(Response(id=str(i), body=str(i))) - response = thread_fixture.push() - self.assertTrue(response.ok, "Failed to push discussion content") - self.setup_thread_page(thread_id) - return thread_id def setup_multiple_threads(self, thread_count, **thread_kwargs): """ @@ -79,32 +61,6 @@ class CohortTestMixin(object): }, }) - def enable_cohorting(self, course_fixture): - """ - Enables cohorting for the specified course fixture. - """ - url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/settings' - data = json.dumps({'is_cohorted': True}) - response = course_fixture.session.patch(url, data=data, headers=course_fixture.headers) - self.assertTrue(response.ok, "Failed to enable cohorts") - - def enable_always_divide_inline_discussions(self, course_fixture): - """ - Enables "always_divide_inline_discussions" (but does not enabling cohorting). - """ - discussions_url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/discussions/settings' - discussions_data = json.dumps({'always_divide_inline_discussions': True}) - course_fixture.session.patch(discussions_url, data=discussions_data, headers=course_fixture.headers) - - def disable_cohorting(self, course_fixture): - """ - Disables cohorting for the specified course fixture. - """ - url = LMS_BASE_URL + "/courses/" + course_fixture._course_key + '/cohorts/settings' - data = json.dumps({'is_cohorted': False}) - response = course_fixture.session.patch(url, data=data, headers=course_fixture.headers) - self.assertTrue(response.ok, "Failed to disable cohorts") - def add_manual_cohort(self, course_fixture, cohort_name): """ Adds a cohort by name, returning its ID. diff --git a/common/test/acceptance/tests/discussion/test_cohort_management.py b/common/test/acceptance/tests/discussion/test_cohort_management.py index 3b40b631b9..50e07f97b1 100644 --- a/common/test/acceptance/tests/discussion/test_cohort_management.py +++ b/common/test/acceptance/tests/discussion/test_cohort_management.py @@ -4,25 +4,14 @@ End-to-end tests related to the cohort management on the LMS Instructor Dashboar """ -import csv -import os -import os.path import uuid -from datetime import datetime - -import six -import unicodecsv -from bok_choy.promise import EmptyPromise -from pytz import UTC, utc from common.test.acceptance.fixtures.course import CourseFixture from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.instructor_dashboard import DataDownloadPage, InstructorDashboardPage -from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage +from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage from common.test.acceptance.tests.discussion.helpers import CohortTestMixin -from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest, create_user_partition_json +from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest from openedx.core.lib.tests import attr -from xmodule.partitions.partitions import Group @attr(shard=8) @@ -71,262 +60,6 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin self.instructor_dashboard_page.visit() self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management() - def verify_cohort_description(self, cohort_name, expected_description): - """ - Selects the cohort with the given name and verifies the expected description is presented. - """ - self.cohort_management_page.select_cohort(cohort_name) - self.assertEqual(self.cohort_management_page.get_selected_cohort(), cohort_name) - self.assertIn(expected_description, self.cohort_management_page.get_cohort_group_setup()) - - def test_cohort_description(self): - """ - Scenario: the cohort configuration management in the instructor dashboard specifies whether - students are automatically or manually assigned to specific cohorts. - - Given I have a course with a manual cohort and an automatic cohort defined - When I view the manual cohort in the instructor dashboard - There is text specifying that students are only added to the cohort manually - And when I view the automatic cohort in the instructor dashboard - There is text specifying that students are automatically added to the cohort - """ - self.verify_cohort_description( - self.manual_cohort_name, - 'Learners are added to this cohort only when you provide ' - 'their email addresses or usernames on this page', - ) - self.verify_cohort_description( - self.auto_cohort_name, - 'Learners are added to this cohort automatically', - ) - - def test_no_content_groups(self): - """ - Scenario: if the course has no content groups defined (user_partitions of type cohort), - the settings in the cohort management tab reflect this - - Given I have a course with a cohort defined but no content groups - When I view the cohort in the instructor dashboard and select settings - Then the cohort is not linked to a content group - And there is text stating that no content groups are defined - And I cannot select the radio button to enable content group association - And there is a link I can select to open Group settings in Studio - """ - self.cohort_management_page.select_cohort(self.manual_cohort_name) - self.assertIsNone(self.cohort_management_page.get_cohort_associated_content_group()) - self.assertEqual( - "Warning:\nNo content groups exist. Create a content group", - self.cohort_management_page.get_cohort_related_content_group_message() - ) - self.assertFalse(self.cohort_management_page.select_content_group_radio_button()) - self.cohort_management_page.select_studio_group_settings() - group_settings_page = GroupConfigurationsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - group_settings_page.wait_for_page() - - def test_add_students_to_cohort_success(self): - """ - Scenario: When students are added to a cohort, the appropriate notification is shown. - - Given I have a course with two cohorts - And there is a user in one cohort - And there is a user in neither cohort - When I add the two users to the cohort that initially had no users - Then there are 2 users in total in the cohort - And I get a notification that 2 users have been added to the cohort - And I get a notification that 1 user was moved from the other cohort - And the user input field is empty - And appropriate events have been emitted - """ - start_time = datetime.now(UTC) - self.cohort_management_page.select_cohort(self.auto_cohort_name) - self.assertEqual(0, self.cohort_management_page.get_selected_cohort_count()) - self.cohort_management_page.add_students_to_selected_cohort([self.student_name, self.instructor_name]) - # Wait for the number of users in the cohort to change, indicating that the add operation is complete. - EmptyPromise( - lambda: 2 == self.cohort_management_page.get_selected_cohort_count(), 'Waiting for added students' - ).fulfill() - confirmation_messages = self.cohort_management_page.get_cohort_confirmation_messages() - self.assertEqual( - [ - "2 learners have been added to this cohort.", - "1 learner was moved from " + self.manual_cohort_name - ], - confirmation_messages - ) - self.assertEqual("", self.cohort_management_page.get_cohort_student_input_field_value()) - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_added", - "time": {"$gt": start_time}, - "event.user_id": {"$in": [int(self.instructor_id), int(self.student_id)]}, - "event.cohort_name": self.auto_cohort_name, - }).count(), - 2 - ) - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_removed", - "time": {"$gt": start_time}, - "event.user_id": int(self.student_id), - "event.cohort_name": self.manual_cohort_name, - }).count(), - 1 - ) - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_add_requested", - "time": {"$gt": start_time}, - "event.user_id": int(self.instructor_id), - "event.cohort_name": self.auto_cohort_name, - "event.previous_cohort_name": None, - }).count(), - 1 - ) - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_add_requested", - "time": {"$gt": start_time}, - "event.user_id": int(self.student_id), - "event.cohort_name": self.auto_cohort_name, - "event.previous_cohort_name": self.manual_cohort_name, - }).count(), - 1 - ) - - def test_add_students_to_cohort_failure(self): - """ - Scenario: When errors occur when adding students to a cohort, the appropriate notification is shown. - - Given I have a course with a cohort and a user already in it - When I add the user already in a cohort to that same cohort - And I add a non-existing user to that cohort - Then there is no change in the number of students in the cohort - And I get a notification that one user was already in the cohort - And I get a notification that one user is unknown - And the user input field still contains the incorrect email addresses - """ - self.cohort_management_page.select_cohort(self.manual_cohort_name) - self.assertEqual(1, self.cohort_management_page.get_selected_cohort_count()) - self.cohort_management_page.add_students_to_selected_cohort([self.student_name, "unknown_user"]) - # Wait for notification messages to appear, indicating that the add operation is complete. - EmptyPromise( - lambda: 2 == len(self.cohort_management_page.get_cohort_confirmation_messages()), 'Waiting for notification' - ).fulfill() - self.assertEqual(1, self.cohort_management_page.get_selected_cohort_count()) - - self.assertEqual( - [ - "0 learners have been added to this cohort.", - "1 learner was already in the cohort" - ], - self.cohort_management_page.get_cohort_confirmation_messages() - ) - - self.assertEqual( - [ - "There was an error when trying to add learners:", - "Unknown username: unknown_user" - ], - self.cohort_management_page.get_cohort_error_messages() - ) - self.assertEqual( - self.student_name + ",unknown_user,", - self.cohort_management_page.get_cohort_student_input_field_value() - ) - - def _verify_cohort_settings( - self, - cohort_name, - assignment_type=None, - new_cohort_name=None, - new_assignment_type=None, - verify_updated=False - ): - """ - Create a new cohort and verify the new and existing settings. - """ - start_time = datetime.now(UTC) - self.assertNotIn(cohort_name, self.cohort_management_page.get_cohorts()) - self.cohort_management_page.add_cohort(cohort_name, assignment_type=assignment_type) - self.assertEqual(0, self.cohort_management_page.get_selected_cohort_count()) - # After adding the cohort, it should automatically be selected and its - # assignment_type should be "manual" as this is the default assignment type - _assignment_type = assignment_type or 'manual' - msg = "Waiting for currently selected cohort assignment type" - EmptyPromise( - lambda: _assignment_type == self.cohort_management_page.get_cohort_associated_assignment_type(), msg - ).fulfill() - # Go back to Manage Students Tab - self.cohort_management_page.select_manage_settings() - self.cohort_management_page.add_students_to_selected_cohort([self.instructor_name]) - # Wait for the number of users in the cohort to change, indicating that the add operation is complete. - EmptyPromise( - lambda: 1 == self.cohort_management_page.get_selected_cohort_count(), 'Waiting for student to be added' - ).fulfill() - self.assertFalse(self.cohort_management_page.is_assignment_settings_disabled) - self.assertEqual('', self.cohort_management_page.assignment_settings_message) - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.created", - "time": {"$gt": start_time}, - "event.cohort_name": cohort_name, - }).count(), - 1 - ) - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.creation_requested", - "time": {"$gt": start_time}, - "event.cohort_name": cohort_name, - }).count(), - 1 - ) - - if verify_updated: - self.cohort_management_page.select_cohort(cohort_name) - self.cohort_management_page.select_cohort_settings() - self.cohort_management_page.set_cohort_name(new_cohort_name) - self.cohort_management_page.set_assignment_type(new_assignment_type) - self.cohort_management_page.save_cohort_settings() - - # If cohort name is empty, then we should get/see an error message. - if not new_cohort_name: - confirmation_messages = self.cohort_management_page.get_cohort_settings_messages(type='error') - self.assertEqual( - ["The cohort cannot be saved", "You must specify a name for the cohort"], - confirmation_messages - ) - else: - confirmation_messages = self.cohort_management_page.get_cohort_settings_messages() - self.assertEqual(["Saved cohort"], confirmation_messages) - self.assertEqual(new_cohort_name, self.cohort_management_page.cohort_name_in_header) - self.assertIn(new_cohort_name, self.cohort_management_page.get_cohorts()) - self.assertEqual(1, self.cohort_management_page.get_selected_cohort_count()) - self.assertEqual( - new_assignment_type, - self.cohort_management_page.get_cohort_associated_assignment_type() - ) - - def _create_csv_file(self, filename, csv_text_as_lists): - """ - Create a csv file with the provided list of lists. - - :param filename: this is the name that will be used for the csv file. Its location will - be under the test upload data directory - :param csv_text_as_lists: provide the contents of the csv file int he form of a list of lists - """ - filename = self.instructor_dashboard_page.get_asset_path(filename) - with open(filename, 'w+') as csv_file: - writer = csv.writer(csv_file) if six.PY3 else unicodecsv.writer(csv_file) - for line in csv_text_as_lists: - writer.writerow(line) - self.addCleanup(os.remove, filename) - def _generate_unique_user_data(self): """ Produce unique username and e-mail. @@ -335,344 +68,6 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin unique_email = unique_username + "@example.com" return unique_username, unique_email - def test_add_new_cohort_with_manual_assignment_type(self): - """ - Scenario: A new cohort with manual assignment type can be created, and a student assigned to it. - - Given I have a course with a user in the course - When I add a new manual cohort with manual assignment type to the course via the LMS instructor dashboard - Then the new cohort is displayed and has no users in it - And assignment type of displayed cohort is "manual" - And when I add the user to the new cohort - Then the cohort has 1 user - And appropriate events have been emitted - """ - cohort_name = str(uuid.uuid4().hex[0:20]) - self._verify_cohort_settings(cohort_name=cohort_name, assignment_type='manual') - - def test_add_new_cohort_with_random_assignment_type(self): - """ - Scenario: A new cohort with random assignment type can be created, and a student assigned to it. - - Given I have a course with a user in the course - When I add a new manual cohort with random assignment type to the course via the LMS instructor dashboard - Then the new cohort is displayed and has no users in it - And assignment type of displayed cohort is "random" - And when I add the user to the new cohort - Then the cohort has 1 user - And appropriate events have been emitted - """ - cohort_name = str(uuid.uuid4().hex[0:20]) - self._verify_cohort_settings(cohort_name=cohort_name, assignment_type='random') - - def test_update_existing_cohort_settings(self): - """ - Scenario: Update existing cohort settings(cohort name, assignment type) - - Given I have a course with a user in the course - When I add a new cohort with random assignment type to the course via the LMS instructor dashboard - Then the new cohort is displayed and has no users in it - And assignment type of displayed cohort is "random" - And when I add the user to the new cohort - Then the cohort has 1 user - And appropriate events have been emitted - Then I select the cohort (that you just created) from existing cohorts - Then I change its name and assignment type set to "manual" - Then I Save the settings - And cohort with new name is present in cohorts dropdown list - And cohort assignment type should be "manual" - """ - cohort_name = str(uuid.uuid4().hex[0:20]) - new_cohort_name = '{old}__NEW'.format(old=cohort_name) - self._verify_cohort_settings( - cohort_name=cohort_name, - assignment_type='random', - new_cohort_name=new_cohort_name, - new_assignment_type='manual', - verify_updated=True - ) - - def test_update_existing_cohort_settings_with_empty_cohort_name(self): - """ - Scenario: Update existing cohort settings(cohort name, assignment type). - - Given I have a course with a user in the course - When I add a new cohort with random assignment type to the course via the LMS instructor dashboard - Then the new cohort is displayed and has no users in it - And assignment type of displayed cohort is "random" - And when I add the user to the new cohort - Then the cohort has 1 user - And appropriate events have been emitted - Then I select a cohort from existing cohorts - Then I set its name as empty string and assignment type set to "manual" - And I click on Save button - Then I should see an error message - """ - cohort_name = str(uuid.uuid4().hex[0:20]) - new_cohort_name = '' - self._verify_cohort_settings( - cohort_name=cohort_name, - assignment_type='random', - new_cohort_name=new_cohort_name, - new_assignment_type='manual', - verify_updated=True - ) - - def test_default_cohort_assignment_settings(self): - """ - Scenario: Cohort assignment settings are disabled for default cohort. - - Given I have a course with a user in the course - And I have added a manual cohort - And I have added a random cohort - When I select the random cohort - Then cohort assignment settings are disabled - """ - self.cohort_management_page.select_cohort("AutoCohort1") - self.cohort_management_page.select_cohort_settings() - - self.assertTrue(self.cohort_management_page.is_assignment_settings_disabled) - - message = "There must be one cohort to which students can automatically be assigned." - self.assertEqual(message, self.cohort_management_page.assignment_settings_message) - - def test_cohort_enable_disable(self): - """ - Scenario: Cohort Enable/Disable checkbox related functionality is working as intended. - - Given I have a cohorted course with a user. - And I can see the `Enable Cohorts` checkbox is checked. - And cohort management controls are visible. - When I uncheck the `Enable Cohorts` checkbox. - Then cohort management controls are not visible. - And When I reload the page. - Then I can see the `Enable Cohorts` checkbox is unchecked. - And cohort management controls are not visible. - """ - self.assertTrue(self.cohort_management_page.is_cohorted) - self.assertTrue(self.cohort_management_page.cohort_management_controls_visible()) - self.cohort_management_page.is_cohorted = False - self.assertFalse(self.cohort_management_page.cohort_management_controls_visible()) - self.browser.refresh() - self.cohort_management_page.wait_for_page() - self.assertFalse(self.cohort_management_page.is_cohorted) - self.assertFalse(self.cohort_management_page.cohort_management_controls_visible()) - - def test_link_to_data_download(self): - """ - Scenario: a link is present from the cohort configuration in - the instructor dashboard to the Data Download section. - - Given I have a course with a cohort defined - When I view the cohort in the LMS instructor dashboard - There is a link to take me to the Data Download section of the Instructor Dashboard. - """ - self.cohort_management_page.select_data_download() - data_download_page = DataDownloadPage(self.browser) - data_download_page.wait_for_page() - - def test_cohort_by_csv_both_columns(self): - """ - Scenario: the instructor can upload a file with user and cohort assignments, using both emails and usernames. - - Given I have a course with two cohorts defined - When I go to the cohort management section of the instructor dashboard - I can upload a CSV file with assignments of users to cohorts via both usernames and emails - Then I can download a file with results - And appropriate events have been emitted - """ - csv_contents = [ - ['username', 'email', 'ignored_column', 'cohort'], - [self.instructor_name, '', 'June', 'ManualCohort1'], - ['', self.student_email, 'Spring', 'AutoCohort1'], - [self.other_student_name, '', 'Fall', 'ManualCohort1'], - ] - filename = "cohort_csv_both_columns_1.csv" - self._create_csv_file(filename, csv_contents) - self._verify_csv_upload_acceptable_file(filename) - - def test_cohort_by_csv_only_email(self): - """ - Scenario: the instructor can upload a file with user and cohort assignments, using only emails. - - Given I have a course with two cohorts defined - When I go to the cohort management section of the instructor dashboard - I can upload a CSV file with assignments of users to cohorts via only emails - Then I can download a file with results - And appropriate events have been emitted - """ - csv_contents = [ - ['email', 'cohort'], - [self.instructor_email, 'ManualCohort1'], - [self.student_email, 'AutoCohort1'], - [self.other_student_email, 'ManualCohort1'], - ] - filename = "cohort_csv_emails_only.csv" - self._create_csv_file(filename, csv_contents) - self._verify_csv_upload_acceptable_file(filename) - - def test_cohort_by_csv_only_username(self): - """ - Scenario: the instructor can upload a file with user and cohort assignments, using only usernames. - - Given I have a course with two cohorts defined - When I go to the cohort management section of the instructor dashboard - I can upload a CSV file with assignments of users to cohorts via only usernames - Then I can download a file with results - And appropriate events have been emitted - """ - csv_contents = [ - ['username', 'cohort'], - [self.instructor_name, 'ManualCohort1'], - [self.student_name, 'AutoCohort1'], - [self.other_student_name, 'ManualCohort1'], - ] - filename = "cohort_users_only_username1.csv" - self._create_csv_file(filename, csv_contents) - self._verify_csv_upload_acceptable_file(filename) - - # TODO: Change unicode_hello_in_korean = u'ßßßßßß' to u'안녕하세요', after up gradation of Chrome driver. See TNL-3944 - def test_cohort_by_csv_unicode(self): - """ - Scenario: the instructor can upload a file with user and cohort assignments, using both emails and usernames. - - Given I have a course with two cohorts defined - And I add another cohort with a unicode name - When I go to the cohort management section of the instructor dashboard - I can upload a CSV file with assignments of users to the unicode cohort via both usernames and emails - Then I can download a file with results - - TODO: refactor events verification to handle this scenario. Events verification assumes movements - between other cohorts (manual and auto). - """ - unicode_hello_in_korean = u'ßßßßßß' - self._verify_cohort_settings(cohort_name=unicode_hello_in_korean, assignment_type=None) - csv_contents = [ - ['username', 'email', 'cohort'], - [self.instructor_name, '', unicode_hello_in_korean], - ['', self.student_email, unicode_hello_in_korean], - [self.other_student_name, '', unicode_hello_in_korean] - ] - filename = "cohort_unicode_name.csv" - self._create_csv_file(filename, csv_contents) - self._verify_csv_upload_acceptable_file(filename, skip_events=True) - - def _verify_csv_upload_acceptable_file(self, filename, skip_events=None): - """ - Helper method to verify cohort assignments after a successful CSV upload. - - When skip_events is specified, no assertions are made on events. - """ - start_time = datetime.now(UTC) - self.cohort_management_page.upload_cohort_file(filename) - self._verify_cohort_by_csv_notification( - u"Your file '{}' has been uploaded. Allow a few minutes for processing.".format(filename) - ) - - if not skip_events: - # student_user is moved from manual cohort to auto cohort - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_added", - "time": {"$gt": start_time}, - "event.user_id": {"$in": [int(self.student_id)]}, - "event.cohort_name": self.auto_cohort_name, - }).count(), - 1 - ) - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_removed", - "time": {"$gt": start_time}, - "event.user_id": int(self.student_id), - "event.cohort_name": self.manual_cohort_name, - }).count(), - 1 - ) - # instructor_user (previously unassigned) is added to manual cohort - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_added", - "time": {"$gt": start_time}, - "event.user_id": {"$in": [int(self.instructor_id)]}, - "event.cohort_name": self.manual_cohort_name, - }).count(), - 1 - ) - # other_student_user (previously unassigned) is added to manual cohort - self.assertEqual( - self.event_collection.find({ - "name": "edx.cohort.user_added", - "time": {"$gt": start_time}, - "event.user_id": {"$in": [int(self.other_student_id)]}, - "event.cohort_name": self.manual_cohort_name, - }).count(), - 1 - ) - - # Verify the results can be downloaded. - data_download = self.instructor_dashboard_page.select_data_download() - data_download.wait_for_available_report() - report = data_download.get_available_reports_for_download()[0] - base_file_name = "cohort_results_" - self.assertIn("{}_{}".format( - '_'.join([self.course_info['org'], self.course_info['number'], self.course_info['run']]), base_file_name - ), report) - report_datetime = datetime.strptime( - report[report.index(base_file_name) + len(base_file_name):-len(".csv")], - "%Y-%m-%d-%H%M" - ) - self.assertLessEqual(start_time.replace(second=0, microsecond=0), utc.localize(report_datetime)) - - def test_cohort_by_csv_wrong_file_type(self): - """ - Scenario: if the instructor uploads a non-csv file, an error message is presented. - - Given I have a course with cohorting enabled - When I go to the cohort management section of the instructor dashboard - And I upload a file without the CSV extension - Then I get an error message stating that the file must have a CSV extension - """ - self.cohort_management_page.upload_cohort_file("image.jpg") - self._verify_cohort_by_csv_notification("The file must end with the extension '.csv'.") - - def test_cohort_by_csv_missing_cohort(self): - """ - Scenario: if the instructor uploads a csv file with no cohort column, an error message is presented. - - Given I have a course with cohorting enabled - When I go to the cohort management section of the instructor dashboard - And I upload a CSV file that is missing the cohort column - Then I get an error message stating that the file must have a cohort column - """ - self.cohort_management_page.upload_cohort_file("cohort_users_missing_cohort_column.csv") - self._verify_cohort_by_csv_notification("The file must contain a 'cohort' column containing cohort names.") - - def test_cohort_by_csv_missing_user(self): - """ - Scenario: if the instructor uploads a csv file with no username or email column, an error message is presented. - - Given I have a course with cohorting enabled - When I go to the cohort management section of the instructor dashboard - And I upload a CSV file that is missing both the username and email columns - Then I get an error message stating that the file must have either a username or email column - """ - self.cohort_management_page.upload_cohort_file("cohort_users_missing_user_columns.csv") - self._verify_cohort_by_csv_notification( - "The file must contain a 'username' column, an 'email' column, or both." - ) - - def _verify_cohort_by_csv_notification(self, expected_message): - """ - Helper method to check the CSV file upload notification message. - """ - # Wait for notification message to appear, indicating file has been uploaded. - EmptyPromise( - lambda: 1 == len(self.cohort_management_page.get_csv_messages()), 'Waiting for notification' - ).fulfill() - messages = self.cohort_management_page.get_csv_messages() - self.assertEqual(expected_message, messages[0]) - @attr('a11y') def test_cohorts_management_a11y(self): """ @@ -685,192 +80,3 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin ] }) self.cohort_management_page.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=15) -class CohortContentGroupAssociationTest(UniqueCourseTest, CohortTestMixin): - """ - Tests for linking between content groups and cohort in the instructor dashboard. - """ - - def setUp(self): - """ - Set up a cohorted course with a user_partition of scheme "cohort". - """ - super(CohortContentGroupAssociationTest, self).setUp() - - # create course with single cohort and two content groups (user_partition of type "cohort") - self.cohort_name = "OnlyCohort" - self.course_fixture = CourseFixture(**self.course_info).install() - self.setup_cohort_config(self.course_fixture) - self.cohort_id = self.add_manual_cohort(self.course_fixture, self.cohort_name) - - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - u"user_partitions": [ - create_user_partition_json( - 0, - 'Apples, Bananas', - 'Content Group Partition', - [Group("0", 'Apples'), Group("1", 'Bananas')], - scheme="cohort" - ) - ], - }, - }) - - # login as an instructor - self.instructor_name = "instructor_user" - self.instructor_id = AutoAuthPage( - self.browser, username=self.instructor_name, email="instructor_user@example.com", - course_id=self.course_id, staff=True - ).visit().get_user_id() - - # go to the membership page on the instructor dashboard - self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - self.instructor_dashboard_page.visit() - self.cohort_management_page = self.instructor_dashboard_page.select_cohort_management() - - def test_no_content_group_linked(self): - """ - Scenario: In a course with content groups, cohorts are initially not linked to a content group - - Given I have a course with a cohort defined and content groups defined - When I view the cohort in the instructor dashboard and select settings - Then the cohort is not linked to a content group - And there is no text stating that content groups are undefined - And the content groups are listed in the selector - """ - self.cohort_management_page.select_cohort(self.cohort_name) - self.assertIsNone(self.cohort_management_page.get_cohort_associated_content_group()) - self.assertIsNone(self.cohort_management_page.get_cohort_related_content_group_message()) - self.assertEqual(["Apples", "Bananas"], self.cohort_management_page.get_all_content_groups()) - - def test_link_to_content_group(self): - """ - Scenario: In a course with content groups, cohorts can be linked to content groups - - Given I have a course with a cohort defined and content groups defined - When I view the cohort in the instructor dashboard and select settings - And I link the cohort to one of the content groups and save - Then there is a notification that my cohort has been saved - And when I reload the page - And I view the cohort in the instructor dashboard and select settings - Then the cohort is still linked to the content group - """ - self._link_cohort_to_content_group(self.cohort_name, "Bananas") - self.assertEqual("Bananas", self.cohort_management_page.get_cohort_associated_content_group()) - - def test_unlink_from_content_group(self): - """ - Scenario: In a course with content groups, cohorts can be unlinked from content groups - - Given I have a course with a cohort defined and content groups defined - When I view the cohort in the instructor dashboard and select settings - And I link the cohort to one of the content groups and save - Then there is a notification that my cohort has been saved - And I reload the page - And I view the cohort in the instructor dashboard and select settings - And I unlink the cohort from any content group and save - Then there is a notification that my cohort has been saved - And when I reload the page - And I view the cohort in the instructor dashboard and select settings - Then the cohort is not linked to any content group - """ - self._link_cohort_to_content_group(self.cohort_name, "Bananas") - self.cohort_management_page.set_cohort_associated_content_group(None) - self._verify_settings_saved_and_reload(self.cohort_name) - self.assertEqual(None, self.cohort_management_page.get_cohort_associated_content_group()) - - def test_create_new_cohort_linked_to_content_group(self): - """ - Scenario: In a course with content groups, a new cohort can be linked to a content group - at time of creation. - - Given I have a course with a cohort defined and content groups defined - When I create a new cohort and link it to a content group - Then when I select settings I see that the cohort is linked to the content group - And when I reload the page - And I view the cohort in the instructor dashboard and select settings - Then the cohort is still linked to the content group - """ - new_cohort = "correctly linked cohort" - self._create_new_cohort_linked_to_content_group(new_cohort, "Apples") - self.browser.refresh() - self.cohort_management_page.wait_for_page() - self.cohort_management_page.select_cohort(new_cohort) - self.assertEqual("Apples", self.cohort_management_page.get_cohort_associated_content_group()) - - def test_missing_content_group(self): - """ - Scenario: In a course with content groups, if a cohort is associated with a content group that no longer - exists, a warning message is shown - - Given I have a course with a cohort defined and content groups defined - When I create a new cohort and link it to a content group - And I delete that content group from the course - And I reload the page - And I view the cohort in the instructor dashboard and select settings - Then the settings display a message that the content group no longer exists - And when I select a different content group and save - Then the error message goes away - """ - new_cohort = "linked to missing content group" - self._create_new_cohort_linked_to_content_group(new_cohort, "Apples") - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - u"user_partitions": [ - create_user_partition_json( - 0, - 'Apples, Bananas', - 'Content Group Partition', - [Group("2", 'Pears'), Group("1", 'Bananas')], - scheme="cohort" - ) - ], - }, - }) - self.browser.refresh() - self.cohort_management_page.wait_for_page() - self.cohort_management_page.select_cohort(new_cohort) - self.assertEqual("Deleted Content Group", self.cohort_management_page.get_cohort_associated_content_group()) - self.assertEqual( - ["Bananas", "Pears", "Deleted Content Group"], - self.cohort_management_page.get_all_content_groups() - ) - self.assertEqual( - "Warning:\nThe previously selected content group was deleted. Select another content group.", - self.cohort_management_page.get_cohort_related_content_group_message() - ) - self.cohort_management_page.set_cohort_associated_content_group("Pears") - confirmation_messages = self.cohort_management_page.get_cohort_settings_messages() - self.assertEqual(["Saved cohort"], confirmation_messages) - self.assertIsNone(self.cohort_management_page.get_cohort_related_content_group_message()) - self.assertEqual(["Bananas", "Pears"], self.cohort_management_page.get_all_content_groups()) - - def _create_new_cohort_linked_to_content_group(self, new_cohort, cohort_group): - """ - Creates a new cohort linked to a content group. - """ - self.cohort_management_page.add_cohort(new_cohort, content_group=cohort_group) - self.assertEqual(cohort_group, self.cohort_management_page.get_cohort_associated_content_group()) - - def _link_cohort_to_content_group(self, cohort_name, content_group): - """ - Links a cohort to a content group. Saves the changes and verifies the cohort updated properly. - Then refreshes the page and selects the cohort. - """ - self.cohort_management_page.select_cohort(cohort_name) - self.cohort_management_page.set_cohort_associated_content_group(content_group) - self._verify_settings_saved_and_reload(cohort_name) - - def _verify_settings_saved_and_reload(self, cohort_name): - """ - Verifies the confirmation message indicating that a cohort's settings have been updated. - Then refreshes the page and selects the cohort. - """ - confirmation_messages = self.cohort_management_page.get_cohort_settings_messages() - self.assertEqual(["Saved cohort"], confirmation_messages) - self.browser.refresh() - self.cohort_management_page.wait_for_page() - self.cohort_management_page.select_cohort(cohort_name) diff --git a/common/test/acceptance/tests/discussion/test_cohorts.py b/common/test/acceptance/tests/discussion/test_cohorts.py deleted file mode 100644 index 477fc338f3..0000000000 --- a/common/test/acceptance/tests/discussion/test_cohorts.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Tests related to the cohorting feature. -""" - - -from uuid import uuid4 - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.discussion import DiscussionTabSingleThreadPage, InlineDiscussionPage -from common.test.acceptance.tests.discussion.helpers import BaseDiscussionMixin, BaseDiscussionTestCase, CohortTestMixin -from common.test.acceptance.tests.helpers import UniqueCourseTest -from openedx.core.lib.tests import attr - - -class NonCohortedDiscussionTestMixin(BaseDiscussionMixin): - """ - Mixin for tests of discussion in non-cohorted courses. - """ - def setup_cohorts(self): - """ - No cohorts are desired for this mixin. - """ - pass - - def test_non_cohort_visibility_label(self): - self.setup_thread(1) - self.assertEqual(self.thread_page.get_group_visibility_label(), "This post is visible to everyone.") - - -class CohortedDiscussionTestMixin(BaseDiscussionMixin, CohortTestMixin): - """ - Mixin for tests of discussion in cohorted courses. - """ - def setup_cohorts(self): - """ - Sets up the course to use cohorting with a single defined cohort. - """ - self.setup_cohort_config(self.course_fixture) - self.cohort_1_name = "Cohort 1" - self.cohort_1_id = self.add_manual_cohort(self.course_fixture, self.cohort_1_name) - - def test_cohort_visibility_label(self): - # Must be moderator to view content in a cohort other than your own - AutoAuthPage(self.browser, course_id=self.course_id, roles="Moderator").visit() - self.thread_id = self.setup_thread(1, group_id=self.cohort_1_id) - - # Enable cohorts and verify that the post shows to cohort only. - self.enable_cohorting(self.course_fixture) - self.enable_always_divide_inline_discussions(self.course_fixture) - self.refresh_thread_page(self.thread_id) - self.assertEqual( - self.thread_page.get_group_visibility_label(), - u"This post is visible only to {}.".format(self.cohort_1_name) - ) - - # Disable cohorts and verify that the post now shows as visible to everyone. - self.disable_cohorting(self.course_fixture) - self.refresh_thread_page(self.thread_id) - self.assertEqual(self.thread_page.get_group_visibility_label(), "This post is visible to everyone.") - - -class DiscussionTabSingleThreadTest(BaseDiscussionTestCase): - """ - Tests for the discussion page displaying a single thread. - """ - def setUp(self): - super(DiscussionTabSingleThreadTest, self).setUp() - self.setup_cohorts() - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - def setup_thread_page(self, thread_id): - self.thread_page = DiscussionTabSingleThreadPage(self.browser, self.course_id, self.discussion_id, thread_id) # pylint: disable=attribute-defined-outside-init - self.thread_page.visit() - - # pylint: disable=unused-argument - def refresh_thread_page(self, thread_id): - self.browser.refresh() - self.thread_page.wait_for_page() - - -@attr(shard=5) -class CohortedDiscussionTabSingleThreadTest(DiscussionTabSingleThreadTest, CohortedDiscussionTestMixin): - """ - Tests for the discussion page displaying a single cohorted thread. - """ - # Actual test method(s) defined in CohortedDiscussionTestMixin. - pass - - -@attr(shard=5) -class NonCohortedDiscussionTabSingleThreadTest(DiscussionTabSingleThreadTest, NonCohortedDiscussionTestMixin): - """ - Tests for the discussion page displaying a single non-cohorted thread. - """ - # Actual test method(s) defined in NonCohortedDiscussionTestMixin. - pass - - -class InlineDiscussionTest(UniqueCourseTest): - """ - Tests for inline discussions - """ - def setUp(self): - super(InlineDiscussionTest, self).setUp() - self.discussion_id = "test_discussion_{}".format(uuid4().hex) - self.course_fixture = CourseFixture(**self.course_info).add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - XBlockFixtureDesc("vertical", "Test Unit").add_children( - XBlockFixtureDesc( - "discussion", - "Test Discussion", - metadata={"discussion_id": self.discussion_id} - ) - ) - ) - ) - ).install() - self.setup_cohorts() - - self.user_id = AutoAuthPage(self.browser, course_id=self.course_id).visit().get_user_id() - - def setup_thread_page(self, thread_id): - CoursewarePage(self.browser, self.course_id).visit() - self.show_thread(thread_id) - - def show_thread(self, thread_id): - discussion_page = InlineDiscussionPage(self.browser, self.discussion_id) - if not discussion_page.is_discussion_expanded(): - discussion_page.expand_discussion() - self.assertEqual(discussion_page.get_num_displayed_threads(), 1) - discussion_page.show_thread(thread_id) - self.thread_page = discussion_page.thread_page # pylint: disable=attribute-defined-outside-init - - def refresh_thread_page(self, thread_id): - self.browser.refresh() - self.show_thread(thread_id) - - -@attr(shard=5) -class CohortedInlineDiscussionTest(InlineDiscussionTest, CohortedDiscussionTestMixin): - """ - Tests for cohorted inline discussions. - """ - # Actual test method(s) defined in CohortedDiscussionTestMixin. - pass - - -@attr(shard=5) -class NonCohortedInlineDiscussionTest(InlineDiscussionTest, NonCohortedDiscussionTestMixin): - """ - Tests for non-cohorted inline discussions. - """ - # Actual test method(s) defined in NonCohortedDiscussionTestMixin. - pass diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py index d3923fa980..0400be395f 100644 --- a/common/test/acceptance/tests/discussion/test_discussion.py +++ b/common/test/acceptance/tests/discussion/test_discussion.py @@ -3,39 +3,24 @@ Tests for discussion pages """ -import datetime -import time -from unittest import skip from uuid import uuid4 import pytest -from pytz import UTC -import six -from six.moves import map from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.fixtures.discussion import ( Comment, Response, - SearchResult, - SearchResultFixture, SingleThreadViewFixture, Thread, - UserProfileViewFixture ) from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.courseware import CoursewarePage from common.test.acceptance.pages.lms.discussion import ( - DiscussionSortPreferencePage, DiscussionTabHomePage, DiscussionTabSingleThreadPage, - DiscussionUserProfilePage, - InlineDiscussionPage ) -from common.test.acceptance.pages.lms.learner_profile import LearnerProfilePage -from common.test.acceptance.pages.lms.tab_nav import TabNavPage from common.test.acceptance.tests.discussion.helpers import BaseDiscussionMixin, BaseDiscussionTestCase -from common.test.acceptance.tests.helpers import UniqueCourseTest, get_modal_alert, skip_if_browser +from common.test.acceptance.tests.helpers import UniqueCourseTest from openedx.core.lib.tests import attr THREAD_CONTENT_WITH_LATEX = u"""Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt @@ -100,85 +85,6 @@ THREAD_CONTENT_WITH_LATEX = u"""Lorem ipsum dolor sit amet, consectetur adipisci """ -class DiscussionResponsePaginationTestMixin(BaseDiscussionMixin): - """ - A mixin containing tests for response pagination for use by both inline - discussion and the discussion tab - """ - def assert_response_display_correct(self, response_total, displayed_responses): - """ - Assert that various aspects of the display of responses are all correct: - * Text indicating total number of responses - * Presence of "Add a response" button - * Number of responses actually displayed - * Presence and text of indicator of how many responses are shown - * Presence and text of button to load more responses - """ - self.assertEqual( - self.thread_page.get_response_total_text(), - str(response_total) + " responses" - ) - self.assertEqual(self.thread_page.has_add_response_button(), response_total != 0) - self.assertEqual(self.thread_page.get_num_displayed_responses(), displayed_responses) - self.assertEqual( - self.thread_page.get_shown_responses_text(), - ( - None if response_total == 0 else - "Showing all responses" if response_total == displayed_responses else - u"Showing first {} responses".format(displayed_responses) - ) - ) - self.assertEqual( - self.thread_page.get_load_responses_button_text(), - ( - None if response_total == displayed_responses else - "Load all responses" if response_total - displayed_responses < 100 else - "Load next 100 responses" - ) - ) - - def test_pagination_no_responses(self): - self.setup_thread(0) - self.assert_response_display_correct(0, 0) - - def test_pagination_few_responses(self): - self.setup_thread(5) - self.assert_response_display_correct(5, 5) - - def test_pagination_two_response_pages(self): - self.setup_thread(50) - self.assert_response_display_correct(50, 25) - - self.thread_page.load_more_responses() - self.assert_response_display_correct(50, 50) - - def test_pagination_exactly_two_response_pages(self): - self.setup_thread(125) - self.assert_response_display_correct(125, 25) - - self.thread_page.load_more_responses() - self.assert_response_display_correct(125, 125) - - def test_pagination_three_response_pages(self): - self.setup_thread(150) - self.assert_response_display_correct(150, 25) - - self.thread_page.load_more_responses() - self.assert_response_display_correct(150, 125) - - self.thread_page.load_more_responses() - self.assert_response_display_correct(150, 150) - - def test_add_response_button(self): - self.setup_thread(5) - self.assertTrue(self.thread_page.has_add_response_button()) - self.thread_page.click_add_response_button() - - def test_add_response_button_closed_thread(self): - self.setup_thread(5, closed=True) - self.assertFalse(self.thread_page.has_add_response_button()) - - @attr(shard=2) class DiscussionHomePageTest(BaseDiscussionTestCase): """ @@ -193,37 +99,6 @@ class DiscussionHomePageTest(BaseDiscussionTestCase): self.page = DiscussionTabHomePage(self.browser, self.course_id) self.page.visit() - @attr(shard=2) - def test_new_post_button(self): - """ - Scenario: I can create new posts from the Discussion home page. - Given that I am on the Discussion home page - When I click on the 'New Post' button - Then I should be shown the new post form - """ - self.assertIsNotNone(self.page.new_post_button) - self.page.click_new_post_button() - self.assertIsNotNone(self.page.new_post_form) - - def test_receive_update_checkbox(self): - """ - Scenario: I can save the receive update email notification checkbox - on Discussion home page. - Given that I am on the Discussion home page - When I click on the 'Receive update' checkbox - Then it should always shown selected. - """ - receive_updates_selector = '.email-setting' - receive_updates_checkbox = self.page.is_element_visible(receive_updates_selector) - self.assertTrue(receive_updates_checkbox) - - self.assertFalse(self.page.is_checkbox_selected(receive_updates_selector)) - self.page.click_element(receive_updates_selector) - - self.assertTrue(self.page.is_checkbox_selected(receive_updates_selector)) - self.page.refresh_and_wait_for_load() - self.assertTrue(self.page.is_checkbox_selected(receive_updates_selector)) - @attr('a11y') def test_page_accessibility(self): self.page.a11y_audit.config.set_rules({ @@ -237,207 +112,6 @@ class DiscussionHomePageTest(BaseDiscussionTestCase): self.page.a11y_audit.check_for_accessibility_errors() -@attr(shard=2) -class DiscussionNavigationTest(BaseDiscussionTestCase): - """ - Tests for breadcrumbs navigation in the Discussions page nav bar - """ - - def setUp(self): - super(DiscussionNavigationTest, self).setUp() - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - thread_id = "test_thread_{}".format(uuid4().hex) - thread_fixture = SingleThreadViewFixture( - Thread( - id=thread_id, - body=THREAD_CONTENT_WITH_LATEX, - commentable_id=self.discussion_id - ) - ) - thread_fixture.push() - self.thread_page = DiscussionTabSingleThreadPage( - self.browser, - self.course_id, - self.discussion_id, - thread_id - ) - self.thread_page.visit() - - @skip("andya: 10/19/17: re-enable once the failure on Jenkins is determined") - def test_breadcrumbs_push_topic(self): - topic_button = self.thread_page.q( - css=".forum-nav-browse-menu-item[data-discussion-id='{}']".format(self.discussion_id) - ) - self.assertTrue(topic_button.visible) - - topic_button.click() - - # Verify the thread's topic has been pushed to breadcrumbs - breadcrumbs = self.thread_page.q(css=".breadcrumbs .nav-item") - self.assertEqual(len(breadcrumbs), 3) - self.assertEqual(breadcrumbs[2].text, "Topic-Level Student-Visible Label") - - @skip("andya: 10/19/17: re-enable once the failure on Jenkins is determined") - def test_breadcrumbs_back_to_all_topics(self): - topic_button = self.thread_page.q( - css=".forum-nav-browse-menu-item[data-discussion-id='{}']".format(self.discussion_id) - ) - self.assertTrue(topic_button.visible) - topic_button.click() - - # Verify clicking the first breadcrumb takes you back to all topics - self.thread_page.q(css=".breadcrumbs .nav-item")[0].click() - self.assertEqual(len(self.thread_page.q(css=".breadcrumbs .nav-item")), 1) - - def test_breadcrumbs_clear_search(self): - self.thread_page.q(css=".search-input").fill("search text") - self.thread_page.q(css=".search-button").click() - - # Verify that clicking the first breadcrumb clears your search - self.thread_page.q(css=".breadcrumbs .nav-item")[0].click() - self.assertEqual(self.thread_page.q(css=".search-input").text[0], "") - - @skip("andya: 10/19/17: re-enable once the failure on Jenkins is determined") - def test_navigation_and_sorting(self): - """ - Test that after adding the post, user sorting preference is changing properly - and recently added post is shown. - """ - topic_button = self.thread_page.q( - css=".forum-nav-browse-menu-item[data-discussion-id='{}']".format(self.discussion_id) - ) - self.assertTrue(topic_button.visible) - topic_button.click() - sort_page = DiscussionSortPreferencePage(self.browser, self.course_id) - for sort_type in ["votes", "comments", "activity"]: - sort_page.change_sort_preference(sort_type) - # Verify that recently added post titled "dummy thread title" is shown in each sorting preference - self.assertEqual(self.thread_page.q(css=".forum-nav-thread-title").text[0], 'dummy thread title') - - -@attr(shard=19) -class DiscussionTabSingleThreadTest(BaseDiscussionTestCase, DiscussionResponsePaginationTestMixin): - """ - Tests for the discussion page displaying a single thread - """ - - def setUp(self): - super(DiscussionTabSingleThreadTest, self).setUp() - AutoAuthPage(self.browser, course_id=self.course_id).visit() - self.tab_nav = TabNavPage(self.browser) - - def setup_thread_page(self, thread_id): - self.thread_page = self.create_single_thread_page(thread_id) # pylint: disable=attribute-defined-outside-init - self.thread_page.visit() - - @skip("andya: 10/19/17: re-enable once the failure on Jenkins is determined") - def test_mathjax_rendering(self): - thread_id = "test_thread_{}".format(uuid4().hex) - - thread_fixture = SingleThreadViewFixture( - Thread( - id=thread_id, - body=THREAD_CONTENT_WITH_LATEX, - commentable_id=self.discussion_id, - thread_type="discussion" - ) - ) - thread_fixture.push() - self.setup_thread_page(thread_id) - self.assertTrue(self.thread_page.is_discussion_body_visible()) - self.thread_page.verify_mathjax_preview_available() - self.thread_page.verify_mathjax_rendered() - - def test_markdown_reference_link(self): - """ - Check markdown editor renders reference link correctly - and colon(:) in reference link is not converted to %3a - """ - sample_link = "http://example.com/colon:test" - thread_content = """[enter link description here][1]\n[1]: http://example.com/colon:test""" - thread_id = "test_thread_{}".format(uuid4().hex) - thread_fixture = SingleThreadViewFixture( - Thread( - id=thread_id, - body=thread_content, - commentable_id=self.discussion_id, - thread_type="discussion" - ) - ) - thread_fixture.push() - self.setup_thread_page(thread_id) - self.assertEqual(self.thread_page.get_link_href(), sample_link) - - def test_marked_answer_comments(self): - thread_id = "test_thread_{}".format(uuid4().hex) - response_id = "test_response_{}".format(uuid4().hex) - comment_id = "test_comment_{}".format(uuid4().hex) - thread_fixture = SingleThreadViewFixture( - Thread(id=thread_id, commentable_id=self.discussion_id, thread_type="question") - ) - thread_fixture.addResponse( - Response(id=response_id, endorsed=True), - [Comment(id=comment_id)] - ) - thread_fixture.push() - self.setup_thread_page(thread_id) - self.assertFalse(self.thread_page.is_comment_visible(comment_id)) - self.assertFalse(self.thread_page.is_add_comment_visible(response_id)) - self.assertTrue(self.thread_page.is_show_comments_visible(response_id)) - self.thread_page.show_comments(response_id) - self.assertTrue(self.thread_page.is_comment_visible(comment_id)) - self.assertTrue(self.thread_page.is_add_comment_visible(response_id)) - self.assertFalse(self.thread_page.is_show_comments_visible(response_id)) - - def test_discussion_blackout_period(self): - """ - Verify that new discussion can not be started during course blackout period. - - Blackout period is the period between which students cannot post new or contribute - to existing discussions. - """ - now = datetime.datetime.now(UTC) - # Update course advance settings with a valid blackout period. - self.course_fixture.add_advanced_settings( - { - u"discussion_blackouts": { - "value": [ - [ - (now - datetime.timedelta(days=14)).isoformat(), - (now + datetime.timedelta(days=2)).isoformat() - ] - ] - } - } - ) - self.course_fixture._add_advanced_settings() # pylint: disable=protected-access - self.browser.refresh() - thread = Thread(id=uuid4().hex, commentable_id=self.discussion_id) - thread_fixture = SingleThreadViewFixture(thread) - thread_fixture.addResponse( - Response(id="response1"), - [Comment(id="comment1")]) - thread_fixture.push() - self.setup_thread_page(thread.get("id")) - - # Verify that `Add a Post` is not visible on course tab nav. - self.assertFalse(self.tab_nav.has_new_post_button_visible_on_tab()) - - # Verify that `Add a response` button is not visible. - self.assertFalse(self.thread_page.has_add_response_button()) - - # Verify user can not add new responses or modify existing responses. - self.assertFalse(self.thread_page.has_discussion_reply_editor()) - self.assertFalse(self.thread_page.is_response_editable("response1")) - self.assertFalse(self.thread_page.is_response_deletable("response1")) - - # Verify that user can not add new comment to a response or modify existing responses. - self.assertFalse(self.thread_page.is_add_comment_visible("response1")) - self.assertFalse(self.thread_page.is_comment_editable("comment1")) - self.assertFalse(self.thread_page.is_comment_deletable("comment1")) - - class DiscussionTabMultipleThreadTest(BaseDiscussionTestCase, BaseDiscussionMixin): """ Tests for the discussion page with multiple threads @@ -520,22 +194,6 @@ class DiscussionOpenClosedThreadTest(BaseDiscussionTestCase): page.close_open_thread() return page - @attr(shard=2) - def test_originally_open_thread_vote_display(self): - page = self.setup_openclosed_thread_page() - self.assertFalse(page.is_element_visible('.thread-main-wrapper .action-vote')) - self.assertTrue(page.is_element_visible('.thread-main-wrapper .display-vote')) - self.assertFalse(page.is_element_visible('.response_response1 .action-vote')) - self.assertTrue(page.is_element_visible('.response_response1 .display-vote')) - - @attr(shard=2) - def test_originally_closed_thread_vote_display(self): - page = self.setup_openclosed_thread_page(True) - self.assertTrue(page.is_element_visible('.thread-main-wrapper .action-vote')) - self.assertFalse(page.is_element_visible('.thread-main-wrapper .display-vote')) - self.assertTrue(page.is_element_visible('.response_response1 .action-vote')) - self.assertFalse(page.is_element_visible('.response_response1 .display-vote')) - @attr('a11y') def test_page_accessibility(self): page = self.setup_openclosed_thread_page() @@ -563,36 +221,6 @@ class DiscussionOpenClosedThreadTest(BaseDiscussionTestCase): page.a11y_audit.check_for_accessibility_errors() -@attr(shard=2) -class DiscussionCommentDeletionTest(BaseDiscussionTestCase): - """ - Tests for deleting comments displayed beneath responses in the single thread view. - """ - def setup_user(self, roles=[]): - roles_str = ','.join(roles) - self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id() - - def setup_view(self): - view = SingleThreadViewFixture(Thread(id="comment_deletion_test_thread", commentable_id=self.discussion_id)) - view.addResponse( - Response(id="response1"), [ - Comment(id="comment_other_author"), - Comment(id="comment_self_author", user_id=self.user_id, thread_id="comment_deletion_test_thread") - ] - ) - view.push() - - def test_comment_deletion_as_moderator(self): - self.setup_user(roles=['Moderator']) - self.setup_view() - page = self.create_single_thread_page("comment_deletion_test_thread") - page.visit() - self.assertTrue(page.is_comment_deletable("comment_self_author")) - self.assertTrue(page.is_comment_deletable("comment_other_author")) - page.delete_comment("comment_self_author") - page.delete_comment("comment_other_author") - - class DiscussionResponseEditTest(BaseDiscussionTestCase): """ Tests for editing responses displayed beneath thread in the single thread view. @@ -611,227 +239,6 @@ class DiscussionResponseEditTest(BaseDiscussionTestCase): ) view.push() - def edit_response(self, page, response_id): - self.assertTrue(page.is_response_editable(response_id)) - page.start_response_edit(response_id) - new_response = "edited body" - page.set_response_editor_value(response_id, new_response) - page.submit_response_edit(response_id, new_response) - - @attr(shard=2) - def test_edit_response_add_link(self): - """ - Scenario: User submits valid input to the 'add link' form - Given I am editing a response on a discussion page - When I click the 'add link' icon in the editor toolbar - And enter a valid url to the URL input field - And enter a valid string in the Description input field - And click the 'OK' button - Then the edited response should contain the new link - """ - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - - response_id = "response_self_author" - url = "http://example.com" - description = "example" - - page.start_response_edit(response_id) - page.set_response_editor_value(response_id, "") - page.add_content_via_editor_button( - "link", response_id, url, description) - page.submit_response_edit(response_id, description) - - expected_response_html = ( - u'

{}

'.format(url, description) - ) - actual_response_html = page.q( - css=u".response_{} .response-body".format(response_id) - ).html[0] - self.assertEqual(expected_response_html, actual_response_html) - - @attr(shard=2) - def test_edit_response_add_image(self): - """ - Scenario: User submits valid input to the 'add image' form - Given I am editing a response on a discussion page - When I click the 'add image' icon in the editor toolbar - And enter a valid url to the URL input field - And enter a valid string in the Description input field - And click the 'OK' button - Then the edited response should contain the new image - """ - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - - response_id = "response_self_author" - url = "http://www.example.com/something.png" - description = "image from example.com" - - page.start_response_edit(response_id) - page.set_response_editor_value(response_id, "") - page.add_content_via_editor_button( - "image", response_id, url, description) - page.submit_response_edit(response_id, '') - - expected_response_html = ( - u'

{}

'.format(url, description) - ) - actual_response_html = page.q( - css=u".response_{} .response-body".format(response_id) - ).html[0] - self.assertEqual(expected_response_html, actual_response_html) - - @attr(shard=2) - def test_edit_response_add_image_error_msg(self): - """ - Scenario: User submits invalid input to the 'add image' form - Given I am editing a response on a discussion page - When I click the 'add image' icon in the editor toolbar - And enter an invalid url to the URL input field - And enter an empty string in the Description input field - And click the 'OK' button - Then I should be shown 2 error messages - """ - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - page.start_response_edit("response_self_author") - page.add_content_via_editor_button( - "image", "response_self_author", '', '') - page.verify_link_editor_error_messages_shown() - - @attr(shard=2) - def test_edit_response_add_decorative_image(self): - """ - Scenario: User submits invalid input to the 'add image' form - Given I am editing a response on a discussion page - When I click the 'add image' icon in the editor toolbar - And enter a valid url to the URL input field - And enter an empty string in the Description input field - And I check the 'image is decorative' checkbox - And click the 'OK' button - Then the edited response should contain the new image - """ - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - - response_id = "response_self_author" - url = "http://www.example.com/something.png" - description = "" - - page.start_response_edit(response_id) - page.set_response_editor_value(response_id, "Some content") - page.add_content_via_editor_button( - "image", response_id, url, description, is_decorative=True) - page.submit_response_edit(response_id, "Some content") - - expected_response_html = ( - u'

Some content{}

'.format( - url, description) - ) - actual_response_html = page.q( - css=u".response_{} .response-body".format(response_id) - ).html[0] - self.assertEqual(expected_response_html, actual_response_html) - - @attr(shard=2) - def test_edit_response_add_link_error_msg(self): - """ - Scenario: User submits invalid input to the 'add link' form - Given I am editing a response on a discussion page - When I click the 'add link' icon in the editor toolbar - And enter an invalid url to the URL input field - And enter an empty string in the Description input field - And click the 'OK' button - Then I should be shown 2 error messages - """ - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - page.start_response_edit("response_self_author") - page.add_content_via_editor_button( - "link", "response_self_author", '', '') - page.verify_link_editor_error_messages_shown() - - @attr(shard=2) - def test_edit_response_as_student(self): - """ - Scenario: Students should be able to edit the response they created not responses of other users - Given that I am on discussion page with student logged in - When I try to edit the response created by student - Then the response should be edited and rendered successfully - And responses from other users should be shown over there - And the student should be able to edit the response of other people - """ - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - self.assertTrue(page.is_response_visible("response_other_author")) - self.assertFalse(page.is_response_editable("response_other_author")) - self.edit_response(page, "response_self_author") - - @attr(shard=2) - def test_edit_response_as_moderator(self): - """ - Scenario: Moderator should be able to edit the response they created and responses of other users - Given that I am on discussion page with moderator logged in - When I try to edit the response created by moderator - Then the response should be edited and rendered successfully - And I try to edit the response created by other users - Then the response should be edited and rendered successfully - """ - self.setup_user(roles=["Moderator"]) - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - self.edit_response(page, "response_self_author") - self.edit_response(page, "response_other_author") - - @attr(shard=2) - def test_vote_report_endorse_after_edit(self): - """ - Scenario: Moderator should be able to vote, report or endorse after editing the response. - Given that I am on discussion page with moderator logged in - When I try to edit the response created by moderator - Then the response should be edited and rendered successfully - And I try to edit the response created by other users - Then the response should be edited and rendered successfully - And I try to vote the response created by moderator - Then the response should not be able to be voted - And I try to vote the response created by other users - Then the response should be voted successfully - And I try to report the response created by moderator - Then the response should not be able to be reported - And I try to report the response created by other users - Then the response should be reported successfully - And I try to endorse the response created by moderator - Then the response should be endorsed successfully - And I try to endorse the response created by other users - Then the response should be endorsed successfully - """ - self.setup_user(roles=["Moderator"]) - self.setup_view() - page = self.create_single_thread_page("response_edit_test_thread") - page.visit() - self.edit_response(page, "response_self_author") - self.edit_response(page, "response_other_author") - page.cannot_vote_response('response_self_author') - page.vote_response('response_other_author') - page.cannot_report_response('response_self_author') - page.report_response('response_other_author') - page.endorse_response('response_self_author') - page.endorse_response('response_other_author') - @attr('a11y') def test_page_accessibility(self): self.setup_user() @@ -864,76 +271,6 @@ class DiscussionCommentEditTest(BaseDiscussionTestCase): [Comment(id="comment_other_author", user_id="other"), Comment(id="comment_self_author", user_id=self.user_id)]) view.push() - def edit_comment(self, page, comment_id): - page.start_comment_edit(comment_id) - new_comment = "edited body" - page.set_comment_editor_value(comment_id, new_comment) - page.submit_comment_edit(comment_id, new_comment) - - @attr(shard=2) - def test_edit_comment_as_student(self): - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("comment_edit_test_thread") - page.visit() - self.assertTrue(page.is_comment_editable("comment_self_author")) - self.assertTrue(page.is_comment_visible("comment_other_author")) - self.assertFalse(page.is_comment_editable("comment_other_author")) - self.edit_comment(page, "comment_self_author") - - @attr(shard=2) - def test_edit_comment_as_moderator(self): - self.setup_user(roles=["Moderator"]) - self.setup_view() - page = self.create_single_thread_page("comment_edit_test_thread") - page.visit() - self.assertTrue(page.is_comment_editable("comment_self_author")) - self.assertTrue(page.is_comment_editable("comment_other_author")) - self.edit_comment(page, "comment_self_author") - self.edit_comment(page, "comment_other_author") - - @attr(shard=2) - def test_cancel_comment_edit(self): - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("comment_edit_test_thread") - page.visit() - self.assertTrue(page.is_comment_editable("comment_self_author")) - original_body = page.get_comment_body("comment_self_author") - page.start_comment_edit("comment_self_author") - page.set_comment_editor_value("comment_self_author", "edited body") - page.cancel_comment_edit("comment_self_author", original_body) - - @attr(shard=2) - def test_editor_visibility(self): - """Only one editor should be visible at a time within a single response""" - self.setup_user(roles=["Moderator"]) - self.setup_view() - page = self.create_single_thread_page("comment_edit_test_thread") - page.visit() - self.assertTrue(page.is_comment_editable("comment_self_author")) - self.assertTrue(page.is_comment_editable("comment_other_author")) - self.assertTrue(page.is_add_comment_visible("response1")) - original_body = page.get_comment_body("comment_self_author") - page.start_comment_edit("comment_self_author") - self.assertFalse(page.is_add_comment_visible("response1")) - self.assertTrue(page.is_comment_editor_visible("comment_self_author")) - page.set_comment_editor_value("comment_self_author", "edited body") - page.start_comment_edit("comment_other_author") - self.assertFalse(page.is_comment_editor_visible("comment_self_author")) - self.assertTrue(page.is_comment_editor_visible("comment_other_author")) - self.assertEqual(page.get_comment_body("comment_self_author"), original_body) - page.start_response_edit("response1") - self.assertFalse(page.is_comment_editor_visible("comment_other_author")) - self.assertTrue(page.is_response_editor_visible("response1")) - original_body = page.get_comment_body("comment_self_author") - page.start_comment_edit("comment_self_author") - self.assertFalse(page.is_response_editor_visible("response1")) - self.assertTrue(page.is_comment_editor_visible("comment_self_author")) - page.cancel_comment_edit("comment_self_author", original_body) - self.assertFalse(page.is_comment_editor_visible("comment_self_author")) - self.assertTrue(page.is_add_comment_visible("response1")) - @attr('a11y') def test_page_accessibility(self): self.setup_user() @@ -950,138 +287,6 @@ class DiscussionCommentEditTest(BaseDiscussionTestCase): }) page.a11y_audit.check_for_accessibility_errors() - -@attr(shard=2) -class DiscussionEditorPreviewTest(UniqueCourseTest): - def setUp(self): - super(DiscussionEditorPreviewTest, self).setUp() - CourseFixture(**self.course_info).install() - AutoAuthPage(self.browser, course_id=self.course_id).visit() - self.page = DiscussionTabHomePage(self.browser, self.course_id) - self.page.visit() - self.page.click_new_post_button() - - # sleep/wait added to allow Major MathJax a11y files to load - time.sleep(5) - - def test_text_rendering(self): - """When I type plain text into the editor, it should be rendered as plain text in the preview box""" - self.page.set_new_post_editor_value("Some plain text") - self.assertEqual(self.page.get_new_post_preview_value(), "

Some plain text

") - - def test_markdown_rendering(self): - """When I type Markdown into the editor, it should be rendered as formatted Markdown in the preview box""" - self.page.set_new_post_editor_value( - "Some markdown\n" - "\n" - "- line 1\n" - "- line 2" - ) - - self.assertEqual(self.page.get_new_post_preview_value(), ( - "

Some markdown

\n" - "\n" - "
    \n" - "
  • line 1
  • \n" - "
  • line 2
  • \n" - "
" - )) - - def test_mathjax_rendering_in_order(self): - """ - Tests that mathjax is rendered in proper order. - - When user types mathjax expressions into discussion editor, it should render in the proper - order. - """ - self.page.set_new_post_editor_value( - 'Text line 1 \n' - '$$e[n]=d_1$$ \n' - 'Text line 2 \n' - '$$e[n]=d_2$$' - ) - self.assertEqual(self.page.get_new_post_preview_text(), - 'Text line 1\ne[n]=\nd\n1\nText line 2\ne[n]=\nd\n2' - ) - - def test_inline_mathjax_rendering_in_order(self): - """ - Tests the order of Post body content when inline Mathjax is used. - - With inline mathjax expressions, the text content doesn't break into new lines at the places of - mathjax expressions. - """ - self.page.set_new_post_editor_value( - 'Text line 1 \n' - '$e[n]=d_1$ \n' - 'Text line 2 \n' - '$e[n]=d_2$' - ) - self.assertEqual(self.page.get_new_post_preview_text('.wmd-preview > p'), - 'Text line 1\ne[n]=\nd\n1\nText line 2\ne[n]=\nd\n2' - ) - - def test_mathjax_not_rendered_after_post_cancel(self): - """ - Tests that mathjax is not rendered when we cancel the post - - When user types the mathjax expression into discussion editor, it will appear in te preview - box, and when user cancel it and again click the "Add new post" button, mathjax will not - appear in the preview box - """ - self.page.set_new_post_editor_value( - six.text_type( - r'\begin{equation}' - r'\tau_g(\omega) = - \frac{d}{d\omega}\phi(\omega) \hspace{2em} (1) ' - r'\end{equation}' - ) - ) - self.assertIsNotNone(self.page.get_new_post_preview_text()) - self.page.click_element(".cancel") - alert = get_modal_alert(self.browser) - alert.accept() - self.assertIsNotNone(self.page.new_post_button) - self.page.click_new_post_button() - self.assertEqual(self.page.get_new_post_preview_value('.wmd-preview'), "") - - -@attr(shard=2) -class InlineDiscussionTest(UniqueCourseTest): - """ - Tests for inline discussions - """ - - def setUp(self): - super(InlineDiscussionTest, self).setUp() - self.thread_ids = [] - self.discussion_id = "test_discussion_{}".format(uuid4().hex) - self.additional_discussion_id = "test_discussion_{}".format(uuid4().hex) - self.course_fix = CourseFixture(**self.course_info).add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - XBlockFixtureDesc("vertical", "Test Unit").add_children( - XBlockFixtureDesc( - "discussion", - "Test Discussion", - metadata={"discussion_id": self.discussion_id} - ), - XBlockFixtureDesc( - "discussion", - "Test Discussion 1", - metadata={"discussion_id": self.additional_discussion_id} - ) - ) - ) - ) - ).install() - - self.user_id = AutoAuthPage(self.browser, course_id=self.course_id).visit().get_user_id() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - self.courseware_page.visit() - self.discussion_page = InlineDiscussionPage(self.browser, self.discussion_id) - self.additional_discussion_page = InlineDiscussionPage(self.browser, self.additional_discussion_id) - @attr('a11y') @pytest.mark.skip(reason='This test is too flaky to run at all. TNL-6215') def test_inline_a11y(self): @@ -1107,208 +312,6 @@ class InlineDiscussionTest(UniqueCourseTest): self.discussion_page.click_new_post_button() self.discussion_page.a11y_audit.check_for_accessibility_errors() - def test_add_a_post_is_present_if_can_create_thread_when_expanded(self): - self.discussion_page.expand_discussion() - # Add a Post link is present - self.assertTrue(self.discussion_page.q(css='.new-post-btn').present) - - def test_add_post_not_present_if_discussion_blackout_period_started(self): - """ - If discussion blackout period has started Add a post button should not appear. - """ - self.start_discussion_blackout_period() - self.browser.refresh() - self.discussion_page.expand_discussion() - self.assertFalse(self.discussion_page.is_new_post_button_visible()) - - def test_initial_render(self): - self.assertTrue(self.discussion_page.is_discussion_expanded()) - - def test_discussion_empty(self): - self.assertEqual(self.discussion_page.get_num_displayed_threads(), 0) - - def test_dual_discussion_xblock(self): - """ - Scenario: Two discussion xblocks in one unit shouldn't override their actions - Given that I'm on a courseware page where there are two inline discussion - When I click on the first discussion block's new post button - Then I should be shown only the new post form for the first block - When I click on the second discussion block's new post button - Then I should be shown both new post forms - When I cancel the first form - Then I should be shown only the new post form for the second block - When I cancel the second form - And I click on the first discussion block's new post button - Then I should be shown only the new post form for the first block - When I cancel the first form - Then I should be shown none of the forms - """ - self.discussion_page.wait_for_page() - self.additional_discussion_page.wait_for_page() - - # Click to add a post to the first discussion - self.discussion_page.click_new_post_button() - - # Verify that only the first discussion's form is shown - self.assertIsNotNone(self.discussion_page.new_post_form) - self.assertIsNone(self.additional_discussion_page.new_post_form) - - # Click to add a post to the second discussion - self.additional_discussion_page.click_new_post_button() - - # Verify that both discussion's forms are shown - self.assertIsNotNone(self.discussion_page.new_post_form) - self.assertIsNotNone(self.additional_discussion_page.new_post_form) - - # Cancel the first form - self.discussion_page.click_cancel_new_post() - - # Verify that only the second discussion's form is shown - self.assertIsNone(self.discussion_page.new_post_form) - self.assertIsNotNone(self.additional_discussion_page.new_post_form) - - # Cancel the second form and click to show the first one - self.additional_discussion_page.click_cancel_new_post() - self.discussion_page.click_new_post_button() - - # Verify that only the first discussion's form is shown - self.assertIsNotNone(self.discussion_page.new_post_form) - self.assertIsNone(self.additional_discussion_page.new_post_form) - - # Cancel the first form - self.discussion_page.click_cancel_new_post() - - # Verify that neither discussion's forms are shwon - self.assertIsNone(self.discussion_page.new_post_form) - self.assertIsNone(self.additional_discussion_page.new_post_form) - - def start_discussion_blackout_period(self): - """ - Start discussion blackout period, starting 14 days before now to 2 days ago. - """ - now = datetime.datetime.now(UTC) - self.course_fix.add_advanced_settings( - { - u"discussion_blackouts": { - "value": [ - [ - (now - datetime.timedelta(days=14)).isoformat(), - (now + datetime.timedelta(days=2)).isoformat() - ] - ] - } - } - ) - self.course_fix._add_advanced_settings() # pylint: disable=protected-access - - -@attr(shard=2) -class DiscussionUserProfileTest(UniqueCourseTest): - """ - Tests for user profile page in discussion tab. - """ - - PAGE_SIZE = 20 # discussion.views.THREADS_PER_PAGE - PROFILED_USERNAME = "profiled-user" - - def setUp(self): - super(DiscussionUserProfileTest, self).setUp() - self.setup_course() - # The following line creates a user enrolled in our course, whose - # threads will be viewed, but not the one who will view the page. - # It isn't necessary to log them in, but using the AutoAuthPage - # saves a lot of code. - self.profiled_user_id = self.setup_user(username=self.PROFILED_USERNAME) - # now create a second user who will view the profile. - self.user_id = self.setup_user() - UserProfileViewFixture([]).push() - - def setup_course(self): - """ - Set up the for the course discussion user-profile tests. - """ - return CourseFixture(**self.course_info).install() - - def setup_user(self, roles=None, **user_info): - """ - Helper method to create and authenticate a user. - """ - roles_str = '' - if roles: - roles_str = ','.join(roles) - return AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str, **user_info).visit().get_user_id() - - def test_redirects_to_learner_profile(self): - """ - Scenario: Verify that learner-profile link is present on forum discussions page and we can navigate to it. - - Given that I am on discussion forum user's profile page. - And I can see a username on the page - When I click on my username. - Then I will be navigated to Learner Profile page. - And I can my username on Learner Profile page - """ - learner_profile_page = LearnerProfilePage(self.browser, self.PROFILED_USERNAME) - - page = DiscussionUserProfilePage( - self.browser, - self.course_id, - self.profiled_user_id, - self.PROFILED_USERNAME - ) - page.visit() - page.click_on_sidebar_username() - - learner_profile_page.wait_for_page() - self.assertTrue(learner_profile_page.field_is_visible('username')) - - def test_learner_profile_roles(self): - """ - Test that on the learner profile page user roles are correctly listed according to the course. - """ - # Setup a learner with roles in a Course-A. - expected_student_roles = ['Administrator', 'Community TA', 'Moderator', 'Student'] - self.profiled_user_id = self.setup_user( - roles=expected_student_roles, - username=self.PROFILED_USERNAME - ) - - # Visit the page and verify the roles are listed correctly. - page = DiscussionUserProfilePage( - self.browser, - self.course_id, - self.profiled_user_id, - self.PROFILED_USERNAME - ) - page.visit() - student_roles = page.get_user_roles() - self.assertEqual(student_roles, ', '.join(expected_student_roles)) - - # Save the course_id of Course-A before setting up a new course. - old_course_id = self.course_id - - # Setup Course-B and set user do not have additional roles and test roles are displayed correctly. - self.course_info['number'] = self.unique_id - self.setup_course() - new_course_id = self.course_id - - # Set the user to have no extra role in the Course-B and verify the existing - # user is updated. - profiled_student_user_id = self.setup_user(roles=None, username=self.PROFILED_USERNAME) - self.assertEqual(self.profiled_user_id, profiled_student_user_id) - self.assertNotEqual(old_course_id, new_course_id) - - # Visit the user profile in course discussion page of Course-B. Make sure the - # roles are listed correctly. - page = DiscussionUserProfilePage( - self.browser, - self.course_id, - self.profiled_user_id, - self.PROFILED_USERNAME - ) - page.visit() - self.assertEqual(page.get_user_roles(), u'Student') - class DiscussionSearchAlertTest(UniqueCourseTest): """ @@ -1331,65 +334,6 @@ class DiscussionSearchAlertTest(UniqueCourseTest): self.page = DiscussionTabHomePage(self.browser, self.course_id) self.page.visit() - def setup_corrected_text(self, text): - SearchResultFixture(SearchResult(corrected_text=text)).push() - - def check_search_alert_messages(self, expected): - actual = self.page.get_search_alert_messages() - self.assertTrue(all(map(lambda msg, sub: msg.lower().find(sub.lower()) >= 0, actual, expected))) - - @attr(shard=2) - def test_no_rewrite(self): - self.setup_corrected_text(None) - self.page.perform_search() - self.check_search_alert_messages(["no posts"]) - - @attr(shard=2) - def test_rewrite_dismiss(self): - self.page.dismiss_alert_message("There are no posts in this topic yet.") - self.setup_corrected_text("foo") - self.page.perform_search() - self.check_search_alert_messages(["foo"]) - self.page.dismiss_alert_message("foo") - self.check_search_alert_messages([]) - - @attr(shard=2) - def test_new_search(self): - self.page.dismiss_alert_message("There are no posts in this topic yet.") - self.setup_corrected_text("foo") - self.page.perform_search() - self.check_search_alert_messages(["foo"]) - - self.setup_corrected_text("bar") - self.page.perform_search() - self.check_search_alert_messages(["bar"]) - - self.setup_corrected_text(None) - self.page.perform_search() - self.check_search_alert_messages(["no posts"]) - - @attr(shard=2) - def test_rewrite_and_user(self): - self.page.dismiss_alert_message("There are no posts in this topic yet.") - self.setup_corrected_text("foo") - self.page.perform_search(self.SEARCHED_USERNAME) - self.check_search_alert_messages(["foo", self.SEARCHED_USERNAME]) - - @attr(shard=2) - def test_user_only(self): - self.setup_corrected_text(None) - self.page.perform_search(self.SEARCHED_USERNAME) - self.check_search_alert_messages(["no posts", self.SEARCHED_USERNAME]) - # make sure clicking the link leads to the user profile page - UserProfileViewFixture([]).push() - self.page.get_search_alert_links().first.click() - DiscussionUserProfilePage( - self.browser, - self.course_id, - self.searched_user_id, - self.SEARCHED_USERNAME - ).wait_for_page() - @attr('a11y') def test_page_accessibility(self): self.page.a11y_audit.config.set_rules({ @@ -1401,57 +345,3 @@ class DiscussionSearchAlertTest(UniqueCourseTest): ] }) self.page.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=2) -class DiscussionSortPreferenceTest(UniqueCourseTest): - """ - Tests for the discussion page displaying a single thread. - """ - - def setUp(self): - super(DiscussionSortPreferenceTest, self).setUp() - - # Create a course to register for. - CourseFixture(**self.course_info).install() - - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - self.sort_page = DiscussionSortPreferencePage(self.browser, self.course_id) - self.sort_page.visit() - self.sort_page.show_all_discussions() - - def test_default_sort_preference(self): - """ - Test to check the default sorting preference of user. (Default = date ) - """ - selected_sort = self.sort_page.get_selected_sort_preference() - self.assertEqual(selected_sort, "activity") - - @skip_if_browser('chrome') # TODO TE-1542 and TE-1543 - def test_change_sort_preference(self): - """ - Test that if user sorting preference is changing properly. - """ - selected_sort = "" - for sort_type in ["votes", "comments", "activity"]: - self.assertNotEqual(selected_sort, sort_type) - self.sort_page.change_sort_preference(sort_type) - selected_sort = self.sort_page.get_selected_sort_preference() - self.assertEqual(selected_sort, sort_type) - - @skip_if_browser('chrome') # TODO TE-1542 and TE-1543 - def test_last_preference_saved(self): - """ - Test that user last preference is saved. - """ - selected_sort = "" - for sort_type in ["votes", "comments", "activity"]: - self.assertNotEqual(selected_sort, sort_type) - self.sort_page.change_sort_preference(sort_type) - selected_sort = self.sort_page.get_selected_sort_preference() - self.assertEqual(selected_sort, sort_type) - self.sort_page.refresh_page() - self.sort_page.show_all_discussions() - selected_sort = self.sort_page.get_selected_sort_preference() - self.assertEqual(selected_sort, sort_type) diff --git a/common/test/acceptance/tests/discussion/test_discussion_management.py b/common/test/acceptance/tests/discussion/test_discussion_management.py deleted file mode 100644 index 73ec992434..0000000000 --- a/common/test/acceptance/tests/discussion/test_discussion_management.py +++ /dev/null @@ -1,495 +0,0 @@ -# -*- coding: utf-8 -*- -""" -End-to-end tests related to the divided discussion management on the LMS Instructor Dashboard -""" - - -import uuid - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.utils import add_enrollment_course_modes -from common.test.acceptance.pages.lms.discussion import DiscussionTabSingleThreadPage -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage -from common.test.acceptance.tests.discussion.helpers import BaseDiscussionMixin, CohortTestMixin -from common.test.acceptance.tests.helpers import UniqueCourseTest -from openedx.core.lib.tests import attr - - -class BaseDividedDiscussionTest(UniqueCourseTest, CohortTestMixin): - """ - Base class for tests related to divided discussions. - """ - def setUp(self): - """ - Set up a discussion topic - """ - super(BaseDividedDiscussionTest, self).setUp() - - self.discussion_id = "test_discussion_{}".format(uuid.uuid4().hex) - self.course_fixture = CourseFixture(**self.course_info).add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - XBlockFixtureDesc("vertical", "Test Unit").add_children( - XBlockFixtureDesc( - "discussion", - "Test Discussion", - metadata={"discussion_id": self.discussion_id} - ) - ) - ) - ) - ).install() - - # create course with single cohort and two content groups (user_partition of type "cohort") - self.cohort_name = "OnlyCohort" - self.setup_cohort_config(self.course_fixture) - self.cohort_id = self.add_manual_cohort(self.course_fixture, self.cohort_name) - - # login as an instructor - self.instructor_name = "instructor_user" - self.instructor_id = AutoAuthPage( - self.browser, username=self.instructor_name, email="instructor_user@example.com", - course_id=self.course_id, staff=True - ).visit().get_user_id() - - # go to the membership page on the instructor dashboard - self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - self.instructor_dashboard_page.visit() - self.discussion_management_page = self.instructor_dashboard_page.select_discussion_management() - self.discussion_management_page.wait_for_page() - - self.course_wide_key = 'course-wide' - self.inline_key = 'inline' - self.scheme_key = 'scheme' - - def check_discussion_topic_visibility(self, visible=True): - """ - Assert that discussion topics are visible with appropriate content. - """ - self.assertEqual(visible, self.discussion_management_page.discussion_topics_visible()) - - if visible: - self.assertEqual( - "Course-Wide Discussion Topics", - self.discussion_management_page.divided_discussion_heading_is_visible(self.course_wide_key) - ) - self.assertTrue(self.discussion_management_page.is_save_button_disabled(self.course_wide_key)) - - self.assertEqual( - "Content-Specific Discussion Topics", - self.discussion_management_page.divided_discussion_heading_is_visible(self.inline_key) - ) - self.assertTrue(self.discussion_management_page.is_save_button_disabled(self.inline_key)) - - def reload_page(self, topics_visible=True): - """ - Refresh the page, then verify if the discussion topics are visible on the discussion - management instructor dashboard tab. - """ - self.browser.refresh() - self.discussion_management_page.wait_for_page() - - self.instructor_dashboard_page.select_discussion_management() - self.discussion_management_page.wait_for_page() - - self.check_discussion_topic_visibility(topics_visible) - - def verify_save_confirmation_message(self, key): - """ - Verify that the save confirmation message for the specified portion of the page is visible. - """ - confirmation_message = self.discussion_management_page.get_divide_discussions_message(key=key) - self.assertIn("Your changes have been saved.", confirmation_message) - - -@attr(shard=15) -class DividedDiscussionTopicsTest(BaseDividedDiscussionTest): - """ - Tests for dividing the inline and course-wide discussion topics. - """ - - def save_and_verify_discussion_topics(self, key): - """ - Saves the discussion topics and the verify the changes. - """ - # click on the inline save button. - self.discussion_management_page.save_discussion_topics(key) - - # verifies that changes saved successfully. - self.verify_save_confirmation_message(key) - - # save button disabled again. - self.assertTrue(self.discussion_management_page.is_save_button_disabled(key)) - - def verify_discussion_topics_after_reload(self, key, divided_topics): - """ - Verifies the changed topics. - """ - self.reload_page() - self.assertEqual(self.discussion_management_page.get_divided_topics_count(key), divided_topics) - - def test_divide_course_wide_discussion_topic(self): - """ - Scenario: divide a course-wide discussion topic. - - Given I have a course with a divide defined, - And a course-wide discussion with disabled Save button. - When I click on the course-wide discussion topic - Then I see the enabled save button - When I click on save button - Then I see success message - When I reload the page - Then I see the discussion topic selected - """ - self.check_discussion_topic_visibility() - - divided_topics_before = self.discussion_management_page.get_divided_topics_count(self.course_wide_key) - self.discussion_management_page.select_discussion_topic(self.course_wide_key) - - self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.course_wide_key)) - - self.save_and_verify_discussion_topics(key=self.course_wide_key) - divided_topics_after = self.discussion_management_page.get_divided_topics_count(self.course_wide_key) - - self.assertNotEqual(divided_topics_before, divided_topics_after) - - self.verify_discussion_topics_after_reload(self.course_wide_key, divided_topics_after) - - def test_always_divide_inline_topic_enabled(self): - """ - Scenario: Select the always_divide_inline_topics radio button - - Given I have a course with a cohort defined, - And an inline discussion topic with disabled Save button. - When I click on always_divide_inline_topics - Then I see enabled save button - And I see disabled inline discussion topics - When I save the change - And I reload the page - Then I see the always_divide_inline_topics option enabled - """ - self.check_discussion_topic_visibility() - - # enable always inline discussion topics and save the change - self.discussion_management_page.select_always_inline_discussion() - self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.inline_key)) - self.assertTrue(self.discussion_management_page.inline_discussion_topics_disabled()) - self.discussion_management_page.save_discussion_topics(key=self.inline_key) - - self.reload_page() - self.assertTrue(self.discussion_management_page.always_inline_discussion_selected()) - - def test_divide_some_inline_topics_enabled(self): - """ - Scenario: Select the divide_some_inline_topics radio button - - Given I have a course with a divide defined and always_divide_inline_topics set to True - And an inline discussion topic with disabled Save button. - When I click on divide_some_inline_topics - Then I see enabled save button - And I see enabled inline discussion topics - When I save the change - And I reload the page - Then I see the divide_some_inline_topics option enabled - """ - self.check_discussion_topic_visibility() - # By default always inline discussion topics is False. Enable it (and reload the page). - self.assertFalse(self.discussion_management_page.always_inline_discussion_selected()) - self.discussion_management_page.select_always_inline_discussion() - self.discussion_management_page.save_discussion_topics(key=self.inline_key) - self.reload_page() - self.assertFalse(self.discussion_management_page.divide_some_inline_discussion_selected()) - - # enable some inline discussion topic radio button. - self.discussion_management_page.select_divide_some_inline_discussion() - # I see that save button is enabled - self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.inline_key)) - # I see that inline discussion topics are enabled - self.assertFalse(self.discussion_management_page.inline_discussion_topics_disabled()) - self.discussion_management_page.save_discussion_topics(key=self.inline_key) - - self.reload_page() - self.assertTrue(self.discussion_management_page.divide_some_inline_discussion_selected()) - - def test_divide_inline_discussion_topic(self): - """ - Scenario: divide inline discussion topic. - - Given I have a course with a divide defined, - And a inline discussion topic with disabled Save button - And When I click on inline discussion topic - And I see enabled save button - And When i click save button - Then I see success message - When I reload the page - Then I see the discussion topic selected - """ - self.check_discussion_topic_visibility() - - divided_topics_before = self.discussion_management_page.get_divided_topics_count(self.inline_key) - # check the discussion topic. - self.discussion_management_page.select_discussion_topic(self.inline_key) - - # Save button enabled. - self.assertFalse(self.discussion_management_page.is_save_button_disabled(self.inline_key)) - - # verifies that changes saved successfully. - self.save_and_verify_discussion_topics(key=self.inline_key) - - divided_topics_after = self.discussion_management_page.get_divided_topics_count(self.inline_key) - self.assertNotEqual(divided_topics_before, divided_topics_after) - - self.verify_discussion_topics_after_reload(self.inline_key, divided_topics_after) - - def test_verify_that_selecting_the_final_child_selects_category(self): - """ - Scenario: Category should be selected on selecting final child. - - Given I have a course with a cohort defined, - And a inline discussion with disabled Save button. - When I click on child topics - Then I see enabled saved button - Then I see parent category to be checked. - """ - self.check_discussion_topic_visibility() - - # category should not be selected. - self.assertFalse(self.discussion_management_page.is_category_selected()) - - # check the discussion topic. - self.discussion_management_page.select_discussion_topic(self.inline_key) - - # verify that category is selected. - self.assertTrue(self.discussion_management_page.is_category_selected()) - - def test_verify_that_deselecting_the_final_child_deselects_category(self): - """ - Scenario: Category should be deselected on deselecting final child. - - Given I have a course with a cohort defined, - And a inline discussion with disabled Save button. - When I click on final child topics - Then I see enabled saved button - Then I see parent category to be deselected. - """ - self.check_discussion_topic_visibility() - - # category should not be selected. - self.assertFalse(self.discussion_management_page.is_category_selected()) - - # check the discussion topic. - self.discussion_management_page.select_discussion_topic(self.inline_key) - - # verify that category is selected. - self.assertTrue(self.discussion_management_page.is_category_selected()) - - # un-check the discussion topic. - self.discussion_management_page.select_discussion_topic(self.inline_key) - - # category should not be selected. - self.assertFalse(self.discussion_management_page.is_category_selected()) - - -@attr(shard=6) -class DivisionSchemeTest(BaseDividedDiscussionTest, BaseDiscussionMixin): - """ - Tests for changing the division scheme for Discussions. - """ - - def add_modes_and_view_discussion_mgmt_page(self, modes): - """ - Adds enrollment modes to the course, and then goes to the - discussion tab on the instructor dashboard. - """ - add_enrollment_course_modes(self.browser, self.course_id, modes) - self.view_discussion_management_page() - - def view_discussion_management_page(self): - """ - Go to the discussion tab on the instructor dashboard. - """ - self.instructor_dashboard_page.visit() - self.assertTrue(self.instructor_dashboard_page.is_discussion_management_visible()) - self.instructor_dashboard_page.select_discussion_management() - self.discussion_management_page.wait_for_page() - - def setup_thread_page(self, thread_id): - """ - This is called by BaseDiscussionMixin.setup_thread. - """ - self.thread_page = DiscussionTabSingleThreadPage( - self.browser, self.course_id, self.discussion_id, thread_id - ) - self.thread_page.visit() - - def test_not_divided_hides_discussion_topics(self): - """ - Tests that discussion topics are hidden iff discussion division is disabled. - """ - # Initially "Cohort" is the selected scheme. - self.assertTrue( - self.discussion_management_page.division_scheme_visible(self.discussion_management_page.COHORT_SCHEME) - ) - self.assertEqual( - self.discussion_management_page.COHORT_SCHEME, - self.discussion_management_page.get_selected_scheme() - ) - self.check_discussion_topic_visibility(visible=True) - - self.discussion_management_page.select_division_scheme(self.discussion_management_page.NOT_DIVIDED_SCHEME) - self.verify_save_confirmation_message(self.scheme_key) - self.check_discussion_topic_visibility(visible=False) - - # Reload the page and make sure that the change was persisted - self.reload_page(topics_visible=False) - self.assertTrue(self.discussion_management_page.division_scheme_visible( - self.discussion_management_page.COHORT_SCHEME) - ) - self.assertEqual( - self.discussion_management_page.NOT_DIVIDED_SCHEME, - self.discussion_management_page.get_selected_scheme() - ) - - # Select "cohort" again and make sure that the discussion topics appear. - self.discussion_management_page.select_division_scheme(self.discussion_management_page.COHORT_SCHEME) - self.verify_save_confirmation_message(self.scheme_key) - self.check_discussion_topic_visibility(visible=True) - - def test_disabling_cohorts(self): - """ - Test that the discussions management tab hides when there is <= 1 enrollment track, the Cohort division scheme - is not selected, and cohorts are disabled. - (even without reloading the page). - """ - self.disable_cohorting(self.course_fixture) - self.instructor_dashboard_page.visit() - self.assertFalse(self.instructor_dashboard_page.is_discussion_management_visible()) - - def test_disabling_cohorts_while_selected(self): - """ - Test that disabling cohorts does not hide the discussion tab when there is more than one enrollment track. - Also that the division scheme for cohorts is visible iff it was selected. - (even without reloading the page). - """ - add_enrollment_course_modes(self.browser, self.course_id, ['audit', 'verified']) - - # Verify that the tab is visible, the cohort scheme is selected by default for divided discussions - self.disable_cohorting(self.course_fixture) - - # Go to Discussions tab and ensure that the correct scheme options are visible - self.view_discussion_management_page() - self.assertTrue( - self.discussion_management_page.division_scheme_visible( - self.discussion_management_page.COHORT_SCHEME - ) - ) - - def test_disabling_cohorts_while_not_selected(self): - """ - Test that disabling cohorts does not hide the discussion tab when there is more than one enrollment track. - Also that the division scheme for cohorts is not visible when cohorts are disabled and another scheme is - selected for division. - (even without reloading the page). - """ - add_enrollment_course_modes(self.browser, self.course_id, ['audit', 'verified']) - - # Verify that the tab is visible - self.view_discussion_management_page() - self.discussion_management_page.select_division_scheme(self.discussion_management_page.ENROLLMENT_TRACK_SCHEME) - self.verify_save_confirmation_message(self.scheme_key) - self.disable_cohorting(self.course_fixture) - - # Go to Discussions tab and ensure that the correct scheme options are visible - self.view_discussion_management_page() - self.assertFalse( - self.discussion_management_page.division_scheme_visible( - self.discussion_management_page.COHORT_SCHEME - ) - ) - - def test_single_enrollment_mode(self): - """ - Test that the enrollment track scheme is not visible if there is a single enrollment mode. - """ - self.add_modes_and_view_discussion_mgmt_page(['audit']) - self.assertFalse( - self.discussion_management_page.division_scheme_visible( - self.discussion_management_page.ENROLLMENT_TRACK_SCHEME - ) - ) - - def test_radio_buttons_with_multiple_enrollment_modes(self): - """ - Test that the enrollment track scheme is visible if there are multiple enrollment tracks, - and that the selection can be persisted. - - Also verifies that the cohort division scheme is not presented if cohorts are disabled and cohorts - are not the selected division scheme. - """ - self.add_modes_and_view_discussion_mgmt_page(['audit', 'verified']) - self.assertTrue( - self.discussion_management_page.division_scheme_visible( - self.discussion_management_page.ENROLLMENT_TRACK_SCHEME - ) - ) - # And the cohort scheme is initially visible because it is selected (and cohorts are enabled). - self.assertTrue( - self.discussion_management_page.division_scheme_visible(self.discussion_management_page.COHORT_SCHEME) - ) - - self.discussion_management_page.select_division_scheme(self.discussion_management_page.ENROLLMENT_TRACK_SCHEME) - self.verify_save_confirmation_message(self.scheme_key) - self.check_discussion_topic_visibility(visible=True) - - # Also disable cohorts so we can verify that the cohort scheme choice goes away. - self.disable_cohorting(self.course_fixture) - - self.reload_page(topics_visible=True) - self.assertEqual( - self.discussion_management_page.ENROLLMENT_TRACK_SCHEME, - self.discussion_management_page.get_selected_scheme() - ) - # Verify that the cohort scheme is no longer visible as cohorts are disabled. - self.assertFalse( - self.discussion_management_page.division_scheme_visible(self.discussion_management_page.COHORT_SCHEME) - ) - - def test_enrollment_track_discussion_visibility_label(self): - """ - If enrollment tracks are the division scheme, verifies that discussion visibility labels - correctly render. - - Note that there are similar tests for cohorts in test_cohorts.py. - """ - def refresh_thread_page(): - self.browser.refresh() - self.thread_page.wait_for_page() - - # Make moderator for viewing all groups in discussions. - AutoAuthPage(self.browser, course_id=self.course_id, roles="Moderator", staff=True).visit() - - self.add_modes_and_view_discussion_mgmt_page(['audit', 'verified']) - self.discussion_management_page.select_division_scheme(self.discussion_management_page.ENROLLMENT_TRACK_SCHEME) - self.verify_save_confirmation_message(self.scheme_key) - # Set "always divide" as the thread we will be creating will be an inline thread, - # and this way the thread does not need to be explicitly divided. - self.enable_always_divide_inline_discussions(self.course_fixture) - - # Create a thread with group_id corresponding to the Audit enrollment mode. - # The Audit group ID is 1, and for the comment service group_id we negate it. - self.setup_thread(1, group_id=-1) - - refresh_thread_page() - self.assertEqual( - self.thread_page.get_group_visibility_label(), - u"This post is visible only to {}.".format("Audit") - ) - - # Disable dividing discussions and verify that the post now shows as visible to everyone. - self.view_discussion_management_page() - self.discussion_management_page.select_division_scheme(self.discussion_management_page.NOT_DIVIDED_SCHEME) - self.verify_save_confirmation_message(self.scheme_key) - - self.thread_page.visit() - self.assertEqual(self.thread_page.get_group_visibility_label(), "This post is visible to everyone.") diff --git a/common/test/acceptance/tests/helpers.py b/common/test/acceptance/tests/helpers.py index 6ccb438a36..1a4f9312be 100644 --- a/common/test/acceptance/tests/helpers.py +++ b/common/test/acceptance/tests/helpers.py @@ -4,14 +4,10 @@ Test helper functions and base classes. import functools -import inspect import io import json -import operator import os -import pprint import sys -from contextlib import contextmanager from datetime import datetime from unittest import SkipTest, TestCase @@ -26,17 +22,13 @@ from path import Path as path from pymongo import ASCENDING, MongoClient from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.select import Select from selenium.webdriver.support.ui import WebDriverWait from six.moves import range, zip from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.common import BASE_URL from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from openedx.core.lib.tests.assertions.events import EventMatchTolerates, assert_event_matches, is_matching_event -from openedx.core.release import RELEASE_LINE, doc_version from xmodule.partitions.partitions import UserPartition MAX_EVENTS_IN_FAILURE_OUTPUT = 20 @@ -307,72 +299,6 @@ def select_option_by_value(browser_query, value, focus_out=False): EmptyPromise(options_selected, "Option is selected").fulfill() -def is_option_value_selected(browser_query, value): - """ - return true if given value is selected in html select element, else return false. - """ - select = Select(browser_query.first.results[0]) - ddl_selected_value = select.first_selected_option.get_attribute('value') - return ddl_selected_value == value - - -def element_has_text(page, css_selector, text): - """ - Return true if the given text is present in the list. - """ - text_present = False - text_list = page.q(css=css_selector).text - - if text_list and (text in text_list): - text_present = True - - return text_present - - -def get_modal_alert(browser): - """ - Returns instance of modal alert box shown in browser after waiting - for 6 seconds - """ - WebDriverWait(browser, 6).until(EC.alert_is_present()) - return browser.switch_to.alert - - -def get_element_padding(page, selector): - """ - Get Padding of the element with given selector, - - :returns a dict object with the following keys. - 1 - padding-top - 2 - padding-right - 3 - padding-bottom - 4 - padding-left - - Example Use: - progress_page.get_element_padding('.wrapper-msg.wrapper-auto-cert') - - """ - js_script = u""" - var $element = $('%(selector)s'); - - element_padding = { - 'padding-top': $element.css('padding-top').replace("px", ""), - 'padding-right': $element.css('padding-right').replace("px", ""), - 'padding-bottom': $element.css('padding-bottom').replace("px", ""), - 'padding-left': $element.css('padding-left').replace("px", "") - }; - - return element_padding; - """ % {'selector': selector} - - return page.browser.execute_script(js_script) - - -def is_404_page(browser): - """ Check if page is 404 """ - return 'Page not found (404)' in browser.find_element_by_tag_name('h1').text - - def create_multiple_choice_xml(correct_choice=2, num_choices=4): """ Return the Multiple Choice Problem XML, given the name of the problem. @@ -411,55 +337,6 @@ def auto_auth(browser, username, email, staff, course_id, **kwargs): AutoAuthPage(browser, username=username, email=email, course_id=course_id, staff=staff, **kwargs).visit() -def assert_link(test, expected_link, actual_link): - """ - Assert that 'href' and text inside help DOM element are correct. - - Arguments: - test: Test on which links are being tested. - expected_link (dict): The expected link attributes. - actual_link (dict): The actual link attribute on page. - """ - test.assertEqual(expected_link['href'], actual_link.get_attribute('href')) - test.assertEqual(expected_link['text'], actual_link.text) - - -def assert_opened_help_link_is_correct(test, url): - """ - Asserts that url of browser when help link is clicked is correct. - Arguments: - test (AcceptanceTest): test calling this method. - url (str): url to verify. - """ - test.browser.switch_to_window(test.browser.window_handles[-1]) - WebDriverWait(test.browser, 10).until(lambda driver: driver.current_url == url) - # Check that the URL loads. Can't do this in the browser because it might - # be loading a "Maze Found" missing content page. - response = requests.get(url) - test.assertEqual(response.status_code, 200, u"URL {!r} returned {}".format(url, response.status_code)) - - -EDX_BOOKS = { - 'course_author': 'edx-partner-course-staff', - 'learner': 'edx-guide-for-students', -} - -OPEN_BOOKS = { - 'course_author': 'open-edx-building-and-running-a-course', - 'learner': 'open-edx-learner-guide', -} - - -def url_for_help(book_slug, path_component): - """ - Create a full help URL given a book slug and a path component. - """ - # Emulate the switch between books that happens in envs/bokchoy.py - books = EDX_BOOKS if RELEASE_LINE == "master" else OPEN_BOOKS - url = 'https://edx.readthedocs.io/projects/{}/en/{}{}'.format(books[book_slug], doc_version(), path_component) - return url - - class EventsTestMixin(TestCase): """ Helpers and setup for running tests that evaluate events emitted @@ -470,271 +347,6 @@ class EventsTestMixin(TestCase): self.event_collection = MongoClient(mongo_host)["test"]["events"] self.start_time = datetime.now() - def reset_event_tracking(self): - """Drop any events that have been collected thus far and start collecting again from scratch.""" - self.event_collection.drop() - self.start_time = datetime.now() - - @contextmanager - def capture_events(self, event_filter=None, number_of_matches=1, captured_events=None): - """ - Context manager that captures all events emitted while executing a particular block. - - All captured events are stored in the list referenced by `captured_events`. Note that this list is appended to - *in place*. The events will be appended to the list in the order they are emitted. - - The `event_filter` is expected to be a callable that allows you to filter the event stream and select particular - events of interest. A dictionary `event_filter` is also supported, which simply indicates that the event should - match that provided expectation. - - `number_of_matches` tells this context manager when enough events have been found and it can move on. The - context manager will not exit until this many events have passed the filter. If not enough events are found - before a timeout expires, then this will raise a `BrokenPromise` error. Note that this simply states that - *at least* this many events have been emitted, so `number_of_matches` is simply a lower bound for the size of - `captured_events`. - """ - start_time = datetime.utcnow() - - yield - - events = self.wait_for_events( - start_time=start_time, event_filter=event_filter, number_of_matches=number_of_matches) - - if captured_events is not None and hasattr(captured_events, 'append') and callable(captured_events.append): - for event in events: - captured_events.append(event) - - @contextmanager - def assert_events_match_during(self, event_filter=None, expected_events=None, in_order=True): - """ - Context manager that ensures that events matching the `event_filter` and `expected_events` are emitted. - - This context manager will filter out the event stream using the `event_filter` and wait for - `len(expected_events)` to match the filter. - - It will then compare the events in order with their counterpart in `expected_events` to ensure they match the - more detailed assertion. - - Typically `event_filter` will be an `event_type` filter and the `expected_events` list will contain more - detailed assertions. - """ - captured_events = [] - with self.capture_events(event_filter, len(expected_events), captured_events): - yield - - self.assert_events_match(expected_events, captured_events, in_order=in_order) - - def wait_for_events(self, start_time=None, event_filter=None, number_of_matches=1, timeout=None): - """ - Wait for `number_of_matches` events to pass the `event_filter`. - - By default, this will look at all events that have been emitted since the beginning of the setup of this mixin. - A custom `start_time` can be specified which will limit the events searched to only those emitted after that - time. - - The `event_filter` is expected to be a callable that allows you to filter the event stream and select particular - events of interest. A dictionary `event_filter` is also supported, which simply indicates that the event should - match that provided expectation. - - `number_of_matches` lets us know when enough events have been found and it can move on. The function will not - return until this many events have passed the filter. If not enough events are found before a timeout expires, - then this will raise a `BrokenPromise` error. Note that this simply states that *at least* this many events have - been emitted, so `number_of_matches` is simply a lower bound for the size of `captured_events`. - - Specifying a custom `timeout` can allow you to extend the default 30 second timeout if necessary. - """ - if start_time is None: - start_time = self.start_time - - if timeout is None: - timeout = 30 - - def check_for_matching_events(): - """Gather any events that have been emitted since `start_time`""" - return self.matching_events_were_emitted( - start_time=start_time, - event_filter=event_filter, - number_of_matches=number_of_matches - ) - - return Promise( - check_for_matching_events, - # This is a bit of a hack, Promise calls str(description), so I set the description to an object with a - # custom __str__ and have it do some intelligent stuff to generate a helpful error message. - CollectedEventsDescription( - u'Waiting for {number_of_matches} events to match the filter:\n{event_filter}'.format( - number_of_matches=number_of_matches, - event_filter=self.event_filter_to_descriptive_string(event_filter), - ), - functools.partial(self.get_matching_events_from_time, start_time=start_time, event_filter={}) - ), - timeout=timeout - ).fulfill() - - def matching_events_were_emitted(self, start_time=None, event_filter=None, number_of_matches=1): - """Return True if enough events have been emitted that pass the `event_filter` since `start_time`.""" - matching_events = self.get_matching_events_from_time(start_time=start_time, event_filter=event_filter) - return len(matching_events) >= number_of_matches, matching_events - - def get_matching_events_from_time(self, start_time=None, event_filter=None): - """ - Return a list of events that pass the `event_filter` and were emitted after `start_time`. - - This function is used internally by most of the other assertions and convenience methods in this class. - - The `event_filter` is expected to be a callable that allows you to filter the event stream and select particular - events of interest. A dictionary `event_filter` is also supported, which simply indicates that the event should - match that provided expectation. - """ - if start_time is None: - start_time = self.start_time - - if isinstance(event_filter, dict): - event_filter = functools.partial(is_matching_event, event_filter) - elif not callable(event_filter): - raise ValueError( - 'event_filter must either be a dict or a callable function with as single "event" parameter that ' - 'returns a boolean value.' - ) - - matching_events = [] - cursor = self.event_collection.find( - { - "time": { - "$gte": start_time - } - } - ).sort("time", ASCENDING) - for event in cursor: - matches = False - try: - # Mongo automatically assigns an _id to all events inserted into it. We strip it out here, since - # we don't care about it. - del event['_id'] - if event_filter is not None: - # Typically we will be grabbing all events of a particular type, however, you can use arbitrary - # logic to identify the events that are of interest. - matches = event_filter(event) - except AssertionError: - # allow the filters to use "assert" to filter out events - continue - else: - if matches is None or matches: - matching_events.append(event) - return matching_events - - def assert_matching_events_were_emitted(self, start_time=None, event_filter=None, number_of_matches=1): - """Assert that at least `number_of_matches` events have passed the filter since `start_time`.""" - description = CollectedEventsDescription( - 'Not enough events match the filter:\n' + self.event_filter_to_descriptive_string(event_filter), - functools.partial(self.get_matching_events_from_time, start_time=start_time, event_filter={}) - ) - - self.assertTrue( - self.matching_events_were_emitted( - start_time=start_time, event_filter=event_filter, number_of_matches=number_of_matches - ), - description - ) - - def assert_no_matching_events_were_emitted(self, event_filter, start_time=None): - """Assert that no events have passed the filter since `start_time`.""" - matching_events = self.get_matching_events_from_time(start_time=start_time, event_filter=event_filter) - - description = CollectedEventsDescription( - 'Events unexpected matched the filter:\n' + self.event_filter_to_descriptive_string(event_filter), - lambda: matching_events - ) - - self.assertEqual(len(matching_events), 0, description) - - def assert_events_match(self, expected_events, actual_events, in_order=True): - """Assert that each actual event matches one of the expected events. - - Args: - expected_events (List): a list of dicts representing the expected events. - actual_events (List): a list of dicts that were actually recorded. - in_order (bool): if True then the events must be in the same order (defaults to True). - """ - if in_order: - for expected_event, actual_event in zip(expected_events, actual_events): - expected_field = (None if expected_event.get('event') is None else - expected_event.get('event').get('field')) - has_field = expected_field is not None - actual_event_to_compare = (next(item for item in actual_events if item.get('event').get('field') == - expected_field)) if has_field else actual_event - - assert_event_matches(expected_event, actual_event_to_compare, tolerate=EventMatchTolerates.lenient()) - else: - for expected_event in expected_events: - actual_event = next(event for event in actual_events if is_matching_event(expected_event, event)) - assert_event_matches( - expected_event, - actual_event or {}, - tolerate=EventMatchTolerates.lenient() - ) - - def relative_path_to_absolute_uri(self, relative_path): - """Return an aboslute URI given a relative path taking into account the test context.""" - return six.moves.urllib.parse.urljoin(BASE_URL, relative_path) - - def event_filter_to_descriptive_string(self, event_filter): - """Find the source code of the callable or pretty-print the dictionary""" - message = '' - if callable(event_filter): - file_name = '(unknown)' - try: - file_name = inspect.getsourcefile(event_filter) - except TypeError: - pass - - try: - list_of_source_lines, line_no = inspect.getsourcelines(event_filter) - except IOError: - pass - else: - message = '{file_name}:{line_no}\n{hr}\n{event_filter}\n{hr}'.format( - event_filter=''.join(list_of_source_lines).rstrip(), - file_name=file_name, - line_no=line_no, - hr='-' * 20, - ) - - if not message: - message = '{hr}\n{event_filter}\n{hr}'.format( - event_filter=pprint.pformat(event_filter), - hr='-' * 20, - ) - - return message - - -class CollectedEventsDescription(object): - """ - Produce a clear error message when tests fail. - - This class calls the provided `get_events_func` when converted to a string, and pretty prints the returned events. - """ - - def __init__(self, description, get_events_func): - self.description = description - self.get_events_func = get_events_func - - def __str__(self): - message_lines = [ - self.description, - 'Events:' - ] - events = self.get_events_func() - events.sort(key=operator.itemgetter('time'), reverse=True) - for event in events[:MAX_EVENTS_IN_FAILURE_OUTPUT]: - message_lines.append(pprint.pformat(event)) - if len(events) > MAX_EVENTS_IN_FAILURE_OUTPUT: - message_lines.append( - 'Too many events to display, the remaining events were omitted. Run locally to diagnose.') - - return '\n\n'.join(message_lines) - class AcceptanceTest(WebAppTest): """ @@ -927,90 +539,3 @@ def create_user_partition_json(partition_id, name, description, groups, scheme=" return UserPartition( partition_id, name, description, groups, MockScheme() ).to_json() - - -def assert_nav_help_link(test, page, href, signed_in=True, close_window=True): - """ - Asserts that help link in navigation bar is correct. - - It first checks the url inside anchor DOM element and - then clicks to ensure that help opens correctly. - - Arguments: - test (AcceptanceTest): Test object - page (PageObject): Page object to perform tests on. - href (str): The help link which we expect to see when it is opened. - signed_in (bool): Specifies whether user is logged in or not. (It affects the css) - close_window(bool): Close the newly-opened help window before continuing - """ - expected_link = { - 'href': href, - 'text': 'Help' - } - # Get actual anchor help element from the page. - actual_link = page.get_nav_help_element_and_click_help(signed_in) - # Assert that 'href' and text are the same as expected. - assert_link(test, expected_link, actual_link) - # Assert that opened link is correct - assert_opened_help_link_is_correct(test, href) - # Close the help window if not kept open intentionally - if close_window: - close_help_window(page) - - -def assert_side_bar_help_link(test, page, href, help_text, as_list_item=False, index=-1, close_window=True): - """ - Asserts that help link in side bar is correct. - - It first checks the url inside anchor DOM element and - then clicks to ensure that help opens correctly. - - Arguments: - test (AcceptanceTest): Test object - page (PageObject): Page object to perform tests on. - href (str): The help link which we expect to see when it is opened. - as_list_item (bool): Specifies whether help element is in one of the - 'li' inside a sidebar list DOM element. - index (int): The index of element in case there are more than - one matching elements. - close_window(bool): Close the newly-opened help window before continuing - """ - expected_link = { - 'href': href, - 'text': help_text - } - # Get actual anchor help element from the page. - actual_link = page.get_side_bar_help_element_and_click_help(as_list_item=as_list_item, index=index) - # Assert that 'href' and text are the same as expected. - assert_link(test, expected_link, actual_link) - # Assert that opened link is correct - assert_opened_help_link_is_correct(test, href) - # Close the help window if not kept open intentionally - if close_window: - close_help_window(page) - - -def close_help_window(page): - """ - Closes the help window - Args: - page (PageObject): Page object to perform tests on. - """ - browser_url = page.browser.current_url - if browser_url.startswith('https://edx.readthedocs.io') or browser_url.startswith('http://edx.readthedocs.io'): - page.browser.close() # close only the current window - page.browser.switch_to_window(page.browser.window_handles[0]) - - -class TestWithSearchIndexMixin(object): - """ Mixin encapsulating search index creation """ - TEST_INDEX_FILENAME = "test_root/index_file.dat" - - def _create_search_index(self): - """ Creates search index backing file """ - with open(self.TEST_INDEX_FILENAME, "w+") as index_file: - json.dump({}, index_file) - - def _cleanup_index_file(self): - """ Removes search index backing file """ - remove_file(self.TEST_INDEX_FILENAME) diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py index 82ec49eccc..550ceb47c3 100644 --- a/common/test/acceptance/tests/lms/test_account_settings.py +++ b/common/test/acceptance/tests/lms/test_account_settings.py @@ -4,16 +4,8 @@ End-to-end tests for the Account Settings page. """ -from datetime import datetime -from unittest import skip - -import six -from bok_choy.page_object import XSS_INJECTION -from pytz import timezone, utc - -from common.test.acceptance.pages.common.auto_auth import FULL_NAME, AutoAuthPage +from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage -from common.test.acceptance.pages.lms.dashboard import DashboardPage from common.test.acceptance.tests.helpers import AcceptanceTest, EventsTestMixin @@ -56,457 +48,6 @@ class AccountSettingsTestMixin(EventsTestMixin, AcceptanceTest): user_id = auto_auth_page.get_user_id() return username, user_id - def settings_changed_event_filter(self, event): - """Filter out any events that are not "settings changed" events.""" - return event['event_type'] == self.USER_SETTINGS_CHANGED_EVENT_NAME - - def expected_settings_changed_event(self, setting, old, new, table=None): - """A dictionary representing the expected fields in a "settings changed" event.""" - return { - 'username': self.username, - 'referer': self.get_settings_page_url(), - 'event': { - 'user_id': self.user_id, - 'setting': setting, - 'old': old, - 'new': new, - 'truncated': [], - 'table': table or 'auth_userprofile' - } - } - - def settings_change_initiated_event_filter(self, event): - """Filter out any events that are not "settings change initiated" events.""" - return event['event_type'] == self.CHANGE_INITIATED_EVENT_NAME - - def expected_settings_change_initiated_event(self, setting, old, new, username=None, user_id=None): - """A dictionary representing the expected fields in a "settings change initiated" event.""" - return { - 'username': username or self.username, - 'referer': self.get_settings_page_url(), - 'event': { - 'user_id': user_id or self.user_id, - 'setting': setting, - 'old': old, - 'new': new, - } - } - - def get_settings_page_url(self): - """The absolute URL of the account settings page given the test context.""" - return self.relative_path_to_absolute_uri(self.ACCOUNT_SETTINGS_REFERER) - - def assert_no_setting_changed_event(self): - """Assert no setting changed event has been emitted thus far.""" - self.assert_no_matching_events_were_emitted({'event_type': self.USER_SETTINGS_CHANGED_EVENT_NAME}) - - -class DashboardMenuTest(AccountSettingsTestMixin, AcceptanceTest): - """ - Tests that the dashboard menu works correctly with the account settings page. - """ - shard = 8 - - def test_link_on_dashboard_works(self): - """ - Scenario: Verify that the "Account" link works from the dashboard. - - - Given that I am a registered user - And I visit my dashboard - And I click on "Account" in the top drop down - Then I should see my account settings page - """ - self.log_in_as_unique_user() - dashboard_page = DashboardPage(self.browser) - dashboard_page.visit() - dashboard_page.click_username_dropdown() - self.assertIn('Account', dashboard_page.username_dropdown_link_text) - dashboard_page.click_account_settings_link() - - -class AccountSettingsPageTest(AccountSettingsTestMixin, AcceptanceTest): - """ - Tests that verify behaviour of the Account Settings page. - """ - SUCCESS_MESSAGE = 'Your changes have been saved.' - shard = 8 - - def setUp(self): - """ - Initialize account and pages. - """ - super(AccountSettingsPageTest, self).setUp() - self.full_name = FULL_NAME - self.social_link = '' - self.username, self.user_id = self.log_in_as_unique_user(full_name=self.full_name) - self.visit_account_settings_page() - - def test_page_view_event(self): - """ - Scenario: An event should be recorded when the "Account Settings" - page is viewed. - - Given that I am a registered user - And I visit my account settings page - Then a page view analytics event should be recorded - """ - - actual_events = self.wait_for_events( - event_filter={'event_type': 'edx.user.settings.viewed'}, number_of_matches=1) - self.assert_events_match( - [ - { - 'event': { - 'user_id': self.user_id, - 'page': 'account', - 'visibility': None - } - } - ], - actual_events - ) - - def test_all_sections_and_fields_are_present(self): - """ - Scenario: Verify that all sections and fields are present on the page. - """ - expected_sections_structure = [ - { - 'title': 'Basic Account Information', - 'fields': [ - 'Username', - 'Full Name', - 'Email Address (Sign In)', - 'Password', - 'Language', - 'Country or Region of Residence', - 'Time Zone', - ] - }, - { - 'title': 'Additional Information', - 'fields': [ - 'Education Completed', - 'Gender', - 'Year of Birth', - 'Preferred Language', - ] - }, - { - 'title': 'Social Media Links', - 'fields': sorted([ - 'Twitter Link', - 'Facebook Link', - 'LinkedIn Link', - ]) - }, - { - 'title': 'Delete My Account', - 'fields': [] - }, - ] - - sections_structure = self.account_settings_page.sections_structure() - sections_structure[2]['fields'] = sorted(sections_structure[2]['fields']) - self.assertEqual(sections_structure, expected_sections_structure) - - def _test_readonly_field(self, field_id, title, value): - """ - Test behavior of a readonly field. - """ - self.assertEqual(self.account_settings_page.title_for_field(field_id), title) - self.assertEqual(self.account_settings_page.value_for_readonly_field(field_id), value) - - def _test_text_field( - self, field_id, title, initial_value, new_invalid_value, new_valid_values, success_message=SUCCESS_MESSAGE, - assert_after_reload=True - ): - """ - Test behaviour of a text field. - """ - self.assertEqual(self.account_settings_page.title_for_field(field_id), title) - self.assertEqual(self.account_settings_page.value_for_text_field(field_id), initial_value) - - self.assertEqual( - self.account_settings_page.value_for_text_field(field_id, new_invalid_value), new_invalid_value - ) - self.account_settings_page.wait_for_indicator(field_id, 'validation-error') - self.browser.refresh() - self.assertNotEqual(self.account_settings_page.value_for_text_field(field_id), new_invalid_value) - - for new_value in new_valid_values: - self.assertEqual(self.account_settings_page.value_for_text_field(field_id, new_value), new_value) - self.account_settings_page.wait_for_message(field_id, success_message) - if assert_after_reload: - self.browser.refresh() - self.assertEqual(self.account_settings_page.value_for_text_field(field_id), new_value) - - def _test_dropdown_field( - self, - field_id, - title, - initial_value, - new_values, - success_message=SUCCESS_MESSAGE, # pylint: disable=unused-argument - reloads_on_save=False - ): - """ - Test behaviour of a dropdown field. - """ - self.assertEqual(self.account_settings_page.title_for_field(field_id), title) - self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id, focus_out=True), initial_value) - - for new_value in new_values: - self.assertEqual( - self.account_settings_page.value_for_dropdown_field(field_id, new_value, focus_out=True), - new_value - ) - # An XHR request is made when changing the field - self.account_settings_page.wait_for_ajax() - if reloads_on_save: - self.account_settings_page.wait_for_loading_indicator() - else: - self.browser.refresh() - self.account_settings_page.wait_for_page() - self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id, focus_out=True), new_value) - - def _test_link_field(self, field_id, title, link_title, field_type, success_message): - """ - Test behaviour a link field. - """ - self.assertEqual(self.account_settings_page.title_for_field(field_id), title) - self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title) - self.account_settings_page.click_on_link_in_link_field(field_id, field_type=field_type) - self.account_settings_page.wait_for_message(field_id, success_message) - - def test_username_field(self): - """ - Test behaviour of "Username" field. - """ - self._test_readonly_field('username', 'Username', self.username) - - def test_full_name_field(self): - """ - Test behaviour of "Full Name" field. - """ - self._test_text_field( - u'name', - u'Full Name', - self.full_name, - u' ', - [u'

another name

', u' - -

What is the sum of $oneseven and 3?

- - - - - - """ - )) - ) - ) - ).install() - - # Auto-auth register for the course - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - def test_python_execution_in_problem(self): - # Navigate to the problem page - self.course_home_page.visit() - self.course_home_page.outline.go_to_section('Test Section', 'Test Subsection') - - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_name.upper(), 'PYTHON PROBLEM') - - # Does the page have computation results? - self.assertIn("What is the sum of 17 and 3?", problem_page.problem_text) - - # Fill in the answer correctly. - problem_page.fill_answer("20") - problem_page.click_submit() - self.assertTrue(problem_page.is_correct()) - - # Fill in the answer incorrectly. - problem_page.fill_answer("4") - problem_page.click_submit() - self.assertFalse(problem_page.is_correct()) - - -@attr(shard=1) -class NotLiveRedirectTest(UniqueCourseTest): - """ - Test that a banner is shown when the user is redirected to - the dashboard from a non-live course. - """ - - def setUp(self): - """Create a course that isn't live yet and enroll for it.""" - super(NotLiveRedirectTest, self).setUp() - CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'], - start_date=datetime(year=2099, month=1, day=1) - ).install() - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - def test_redirect_banner(self): - """ - Navigate to the course info page, then check that we're on the - dashboard page with the appropriate message. - """ - url = BASE_URL + "/courses/" + self.course_id + "/" + 'info' - self.browser.get(url) - page = DashboardPage(self.browser) - page.wait_for_page() - self.assertIn( - 'The course you are looking for does not start until', - page.banner_text - ) - - -@attr(shard=1) -class EnrollmentClosedRedirectTest(UniqueCourseTest): - """ - Test that a banner is shown when the user is redirected to the - dashboard after trying to view the track selection page for a - course after enrollment has ended. - """ - - def setUp(self): - """Create a course that is closed for enrollment, and sign in as a user.""" - super(EnrollmentClosedRedirectTest, self).setUp() - course = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - now = datetime.now(pytz.UTC) - course.add_course_details({ - 'enrollment_start': (now - timedelta(days=30)).isoformat(), - 'enrollment_end': (now - timedelta(days=1)).isoformat() - }) - course.install() - - # Add an honor mode to the course - ModeCreationPage(self.browser, self.course_id).visit() - - # Add a verified mode to the course - ModeCreationPage( - self.browser, - self.course_id, - mode_slug=u'verified', - mode_display_name=u'Verified Certificate', - min_price=10, - suggested_prices='10,20' - ).visit() - - def _assert_dashboard_message(self): - """ - Assert that the 'closed for enrollment' text is present on the - dashboard. - """ - page = DashboardPage(self.browser) - page.wait_for_page() - self.assertIn( - 'The course you are looking for is closed for enrollment', - page.banner_text - ) - - def test_redirect_banner(self): - """ - Navigate to the course info page, then check that we're on the - dashboard page with the appropriate message. - """ - AutoAuthPage(self.browser).visit() - url = BASE_URL + "/course_modes/choose/" + self.course_id - self.browser.get(url) - self._assert_dashboard_message() - - -@attr(shard=19) -class RegisterCourseTests(EventsTestMixin, UniqueCourseTest): - """Test that learner can enroll into a course from courses page""" - - TEST_INDEX_FILENAME = "test_root/index_file.dat" - - def setUp(self): - """ - Initialize the test. - - Create the necessary page objects, create course page and courses to find. - """ - super(RegisterCourseTests, self).setUp() - - # create test file in which index for this test will live - with open(self.TEST_INDEX_FILENAME, "w+") as index_file: - json.dump({}, index_file) - self.addCleanup(remove_file, self.TEST_INDEX_FILENAME) - - self.course_discovery = CourseDiscoveryPage(self.browser) - self.dashboard_page = DashboardPage(self.browser) - self.course_about = CourseAboutPage(self.browser, self.course_id) - - # Create a course - CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'], - settings={'enrollment_start': datetime(1970, 1, 1).isoformat()} - ).install() - - # Create a user and log them in - AutoAuthPage(self.browser).visit() - - def test_register_for_course(self): - """ - Scenario: I can register for a course - Given The course "6.002x" exists - And I am logged in - And I visit the courses page - When I register for the course "6.002x" - Then I should see the course numbered "6.002x" in my dashboard - And a "edx.course.enrollment.activated" server event is emitted - """ - # Navigate to the dashboard - self.course_discovery.visit() - self.course_discovery.click_course(self.course_id) - self.course_about.wait_for_page() - self.course_about.enroll_in_course() - self.dashboard_page.wait_for_page() - self.assertTrue(self.dashboard_page.is_course_present(self.course_id)) - self.assert_matching_events_were_emitted( - event_filter={'name': u'edx.course.enrollment.activated', 'event_source': 'server'} - ) diff --git a/common/test/acceptance/tests/lms/test_lms_acid_xblock.py b/common/test/acceptance/tests/lms/test_lms_acid_xblock.py deleted file mode 100644 index 8c70d71ae1..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_acid_xblock.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -""" -End-to-end tests for the LMS. -""" - - -import pytest - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.xblock.acid import AcidView -from common.test.acceptance.tests.helpers import UniqueCourseTest - - -class XBlockAcidBase(UniqueCourseTest): - """ - Base class for tests that verify that XBlock integration is working correctly - """ - __test__ = False - - def setUp(self): - """ - Create a unique identifier for the course used in this test. - """ - # Ensure that the superclass sets up - super(XBlockAcidBase, self).setUp() - - self.setup_fixtures() - - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - def validate_acid_block_view(self, acid_block): - """ - Verify that the LMS view for the Acid Block is correct - """ - self.assertTrue(acid_block.init_fn_passed) - self.assertTrue(acid_block.resource_url_passed) - self.assertTrue(acid_block.scope_passed('user_state')) - self.assertTrue(acid_block.scope_passed('user_state_summary')) - self.assertTrue(acid_block.scope_passed('preferences')) - self.assertTrue(acid_block.scope_passed('user_info')) - - -class XBlockAcidNoChildTest(XBlockAcidBase): - """ - Tests of an AcidBlock with no children - """ - shard = 20 - __test__ = True - - def setup_fixtures(self): - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid', 'Acid Block') - ) - ) - ) - ).install() - - def test_acid_block(self): - """ - Verify that all expected acid block tests pass in the lms. - """ - self.courseware_page.visit() - acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]') - self.validate_acid_block_view(acid_block) - - -class XBlockAcidChildTest(XBlockAcidBase): - """ - Tests of an AcidBlock with children - """ - shard = 20 - __test__ = True - - def setup_fixtures(self): - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid_parent', 'Acid Parent Block').add_children( - XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}), - XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}), - XBlockFixtureDesc('html', 'Html Child', data="Contents"), - ) - ) - ) - ) - ).install() - - def validate_acid_parent_block_view(self, acid_parent_block): - super(XBlockAcidChildTest, self).validate_acid_block_view(acid_parent_block) - self.assertTrue(acid_parent_block.child_tests_passed) - - def test_acid_block(self): - """ - Verify that all expected acid block tests pass in the lms. - """ - self.courseware_page.visit() - acid_parent_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid_parent]') - self.validate_acid_parent_block_view(acid_parent_block) - - acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]') - self.validate_acid_block_view(acid_block) - - -@pytest.mark.xfail -class XBlockAcidAsideTest(XBlockAcidBase): - """ - Tests of an AcidBlock with children - """ - shard = 20 - __test__ = True - - def setup_fixtures(self): - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid', 'Acid Block') - ) - ) - ) - ).install() - - def test_acid_block(self): - """ - Verify that all expected acid block tests pass in the lms. - """ - self.courseware_page.visit() - acid_aside = AcidView(self.browser, '.xblock_asides-v1-student_view[data-block-type=acid_aside]') - self.validate_acid_aside_view(acid_aside) - - acid_block = AcidView(self.browser, '.xblock-student_view[data-block-type=acid]') - self.validate_acid_block_view(acid_block) - - def validate_acid_aside_view(self, acid_aside): - self.validate_acid_block_view(acid_aside) diff --git a/common/test/acceptance/tests/lms/test_lms_cohorted_courseware_search.py b/common/test/acceptance/tests/lms/test_lms_cohorted_courseware_search.py deleted file mode 100644 index 58b9786f1d..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_cohorted_courseware_search.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -Test courseware search -""" - - -import json -import uuid - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.logout import LogoutPage -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage -from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage -from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage -from common.test.acceptance.pages.studio.xblock_editor import XBlockVisibilityEditorView -from common.test.acceptance.tests.discussion.helpers import CohortTestMixin -from common.test.acceptance.tests.helpers import remove_file -from common.test.acceptance.tests.studio.base_studio_test import ContainerBase - - -class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin): - """ - Test courseware search. - """ - shard = 1 - TEST_INDEX_FILENAME = "test_root/index_file.dat" - - def setUp(self, is_staff=True): - """ - Create search page and course content to search - """ - # create test file in which index for this test will live - with open(self.TEST_INDEX_FILENAME, "w+") as index_file: - json.dump({}, index_file) - self.addCleanup(remove_file, self.TEST_INDEX_FILENAME) - - super(CoursewareSearchCohortTest, self).setUp(is_staff=is_staff) - self.staff_user = self.user - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self.content_group_a = "Content Group A" - self.content_group_b = "Content Group B" - - # Create a student who will be in "Cohort A" - self.cohort_a_student_username = "cohort_a_" + str(uuid.uuid4().hex)[:12] - self.cohort_a_student_email = self.cohort_a_student_username + "@example.com" - AutoAuthPage( - self.browser, username=self.cohort_a_student_username, email=self.cohort_a_student_email, no_login=True - ).visit() - - # Create a student who will be in "Cohort B" - self.cohort_b_student_username = "cohort_b_" + str(uuid.uuid4().hex)[:12] - self.cohort_b_student_email = self.cohort_b_student_username + "@example.com" - AutoAuthPage( - self.browser, username=self.cohort_b_student_username, email=self.cohort_b_student_email, no_login=True - ).visit() - - # Create a student who will end up in the default cohort group - self.cohort_default_student_username = "cohort_default_student" - self.cohort_default_student_email = "cohort_default_student@example.com" - AutoAuthPage( - self.browser, username=self.cohort_default_student_username, - email=self.cohort_default_student_email, no_login=True - ).visit() - - self.course_home_page = CourseHomePage(self.browser, self.course_id) - - # Enable Cohorting and assign cohorts and content groups - self._auto_auth(self.staff_user["username"], self.staff_user["email"], True) - self.enable_cohorting(self.course_fixture) - self.create_content_groups() - self.link_html_to_content_groups_and_publish() - self.create_cohorts_and_assign_students() - - self._studio_reindex() - - def _auto_auth(self, username, email, staff): - """ - Logout and login with given credentials. - """ - LogoutPage(self.browser).visit() - AutoAuthPage(self.browser, username=username, email=email, - course_id=self.course_id, staff=staff).visit() - - def _studio_reindex(self): - """ - Reindex course content on studio course page - """ - self._auto_auth(self.staff_user["username"], self.staff_user["email"], True) - self.studio_course_outline.visit() - self.studio_course_outline.start_reindex() - self.studio_course_outline.wait_for_ajax() - - def _goto_staff_page(self): - """ - Open staff page with assertion - """ - self.course_home_page.visit() - self.course_home_page.resume_course_from_header() - staff_page = StaffCoursewarePage(self.browser, self.course_id) - self.assertEqual(staff_page.staff_view_mode, 'Staff') - return staff_page - - def _search_for_term(self, term): - """ - Search for term in course and return results. - """ - self.course_home_page.visit() - course_search_results_page = self.course_home_page.search_for_term(term) - results = course_search_results_page.search_results.html - return results[0] if len(results) > 0 else [] - - def populate_course_fixture(self, course_fixture): - """ - Populate the children of the test course fixture. - """ - self.group_a_html = 'GROUPACONTENT' - self.group_b_html = 'GROUPBCONTENT' - self.group_a_and_b_html = 'GROUPAANDBCONTENT' - self.visible_to_all_html = 'VISIBLETOALLCONTENT' - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('html', self.group_a_html, data='GROUPACONTENT'), - XBlockFixtureDesc('html', self.group_b_html, data='GROUPBCONTENT'), - XBlockFixtureDesc('html', self.group_a_and_b_html, data='GROUPAANDBCONTENT'), - XBlockFixtureDesc('html', self.visible_to_all_html, data='VISIBLETOALLCONTENT') - ) - ) - ) - ) - - def create_content_groups(self): - """ - Creates two content groups in Studio Group Configurations Settings. - """ - group_configurations_page = GroupConfigurationsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - group_configurations_page.visit() - - group_configurations_page.create_first_content_group() - config = group_configurations_page.content_groups[0] - config.name = self.content_group_a - config.save() - - group_configurations_page.add_content_group() - config = group_configurations_page.content_groups[1] - config.name = self.content_group_b - config.save() - - def link_html_to_content_groups_and_publish(self): - """ - Updates 3 of the 4 existing html to limit their visibility by content group. - Publishes the modified units. - """ - container_page = self.go_to_unit_page() - - def set_visibility(html_block_index, groups): - """ - Set visibility on html blocks to specified groups. - """ - html_block = container_page.xblocks[html_block_index] - html_block.edit_visibility() - visibility_dialog = XBlockVisibilityEditorView(self.browser, html_block.locator) - visibility_dialog.select_groups_in_partition_scheme(visibility_dialog.CONTENT_GROUP_PARTITION, groups) - - set_visibility(1, [self.content_group_a]) - set_visibility(2, [self.content_group_b]) - set_visibility(3, [self.content_group_a, self.content_group_b]) - - container_page.publish() - - def create_cohorts_and_assign_students(self): - """ - Adds 2 manual cohorts, linked to content groups, to the course. - Each cohort is assigned one student. - """ - instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - instructor_dashboard_page.visit() - cohort_management_page = instructor_dashboard_page.select_cohort_management() - - def add_cohort_with_student(cohort_name, content_group, student): - """ - Create cohort and assign student to it. - """ - cohort_management_page.add_cohort(cohort_name, content_group=content_group) - cohort_management_page.add_students_to_selected_cohort([student]) - add_cohort_with_student("Cohort A", self.content_group_a, self.cohort_a_student_username) - add_cohort_with_student("Cohort B", self.content_group_b, self.cohort_b_student_username) - cohort_management_page.wait_for_ajax() - - def test_cohorted_search_user_a_a_content(self): - """ - Test user can search content restricted to his cohort. - """ - self._auto_auth(self.cohort_a_student_username, self.cohort_a_student_email, False) - search_results = self._search_for_term(self.group_a_html) - assert self.group_a_html in search_results - - def test_cohorted_search_user_b_a_content(self): - """ - Test user can not search content restricted to his cohort. - """ - self._auto_auth(self.cohort_b_student_username, self.cohort_b_student_email, False) - search_results = self._search_for_term(self.group_a_html) - assert self.group_a_html not in search_results - - def test_cohorted_search_user_staff_all_content(self): - """ - Test staff user can search all public content if cohorts used on course. - """ - self._auto_auth(self.staff_user["username"], self.staff_user["email"], False) - self._goto_staff_page().set_staff_view_mode('Staff') - search_results = self._search_for_term(self.visible_to_all_html) - assert self.visible_to_all_html in search_results - search_results = self._search_for_term(self.group_a_and_b_html) - assert self.group_a_and_b_html in search_results - search_results = self._search_for_term(self.group_a_html) - assert self.group_a_html in search_results - search_results = self._search_for_term(self.group_b_html) - assert self.group_b_html in search_results - - def test_cohorted_search_user_staff_masquerade_student_content(self): - """ - Test staff user can search just student public content if selected from preview menu. - - NOTE: Although it would be wise to combine these masquerading tests into - a single test due to expensive setup, doing so revealed a very low - priority bug where searching seems to stick/cache the access of the - first user who searches for future searches. - - """ - self._auto_auth(self.staff_user["username"], self.staff_user["email"], False) - self._goto_staff_page().set_staff_view_mode('Learner') - search_results = self._search_for_term(self.visible_to_all_html) - assert self.visible_to_all_html in search_results - search_results = self._search_for_term(self.group_a_and_b_html) - assert self.group_a_and_b_html not in search_results - search_results = self._search_for_term(self.group_a_html) - assert self.group_a_html not in search_results - search_results = self._search_for_term(self.group_b_html) - assert self.group_b_html not in search_results - - def test_cohorted_search_user_staff_masquerade_cohort_content(self): - """ - Test staff user can search cohort and public content if selected from preview menu. - """ - self._auto_auth(self.staff_user["username"], self.staff_user["email"], False) - self._goto_staff_page().set_staff_view_mode('Learner in ' + self.content_group_a) - search_results = self._search_for_term(self.visible_to_all_html) - assert self.visible_to_all_html in search_results - search_results = self._search_for_term(self.group_a_and_b_html) - assert self.group_a_and_b_html in search_results - search_results = self._search_for_term(self.group_a_html) - assert self.group_a_html in search_results - search_results = self._search_for_term(self.group_b_html) - assert self.group_b_html not in search_results diff --git a/common/test/acceptance/tests/lms/test_lms_course_discovery.py b/common/test/acceptance/tests/lms/test_lms_course_discovery.py deleted file mode 100644 index dc827e8dc8..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_course_discovery.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Test course discovery. -""" - - -import datetime -import json -import uuid - -from six.moves import range - -from common.test.acceptance.fixtures.course import CourseFixture -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.logout import LogoutPage -from common.test.acceptance.pages.lms.discovery import CourseDiscoveryPage -from common.test.acceptance.tests.helpers import AcceptanceTest, remove_file - - -class CourseDiscoveryTest(AcceptanceTest): - """ - Test searching for courses. - """ - shard = 20 - - STAFF_USERNAME = "STAFF_TESTER" - STAFF_EMAIL = "staff101@example.com" - TEST_INDEX_FILENAME = "test_root/index_file.dat" - - def setUp(self): - """ - Create course page and courses to find - """ - # create index file - with open(self.TEST_INDEX_FILENAME, "w+") as index_file: - json.dump({}, index_file) - - self.addCleanup(remove_file, self.TEST_INDEX_FILENAME) - - super(CourseDiscoveryTest, self).setUp() - self.page = CourseDiscoveryPage(self.browser) - - for i in range(12): - org = 'test_org' - number = "{}{}".format(str(i), str(uuid.uuid4().hex.upper()[0:6])) - run = "test_run" - name = "test course" if i < 10 else "grass is always greener" - settings = {'enrollment_start': datetime.datetime(1970, 1, 1).isoformat()} - CourseFixture(org, number, run, name, settings=settings).install() - - def _auto_auth(self, username, email, staff): - """ - Logout and login with given credentials. - """ - LogoutPage(self.browser).visit() - AutoAuthPage(self.browser, username=username, email=email, staff=staff).visit() - - def test_page_existence(self): - """ - Make sure that the page is accessible. - """ - self.page.visit() - - def test_search(self): - """ - Make sure you can search for courses. - """ - self.page.visit() - self.assertEqual(len(self.page.result_items), 12) - - self.page.search("grass") - self.assertEqual(len(self.page.result_items), 2) - - self.page.clear_search() - self.assertEqual(len(self.page.result_items), 12) diff --git a/common/test/acceptance/tests/lms/test_lms_course_home.py b/common/test/acceptance/tests/lms/test_lms_course_home.py index a298398c47..a161dbf37a 100644 --- a/common/test/acceptance/tests/lms/test_lms_course_home.py +++ b/common/test/acceptance/tests/lms/test_lms_course_home.py @@ -3,16 +3,9 @@ End-to-end tests for the LMS that utilize the course home page and course outline. """ - -from datetime import datetime, timedelta - -import six - -from common.test.acceptance.pages.lms.create_mode import ModeCreationPage from openedx.core.lib.tests import attr from ...fixtures.course import CourseFixture, XBlockFixtureDesc -from ...pages.lms.bookmarks import BookmarksPage from ...pages.lms.course_home import CourseHomePage from ...pages.lms.courseware import CoursewarePage from ..helpers import UniqueCourseTest, auto_auth, load_data_str diff --git a/common/test/acceptance/tests/lms/test_lms_courseware.py b/common/test/acceptance/tests/lms/test_lms_courseware.py deleted file mode 100644 index 7afb955c41..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_courseware.py +++ /dev/null @@ -1,646 +0,0 @@ -# -*- coding: utf-8 -*- -""" -End-to-end tests for the LMS. -""" - - -import json -from datetime import datetime, timedelta - -from six.moves import range - -from openedx.core.lib.tests import attr - -from ...fixtures.course import CourseFixture, XBlockFixtureDesc -from ...pages.common.auto_auth import AutoAuthPage -from ...pages.common.logout import LogoutPage -from ...pages.lms.course_home import CourseHomePage -from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage -from ...pages.lms.problem import ProblemPage -from ...pages.lms.progress import ProgressPage -from ...pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage -from ..helpers import EventsTestMixin, UniqueCourseTest, auto_auth, create_multiple_choice_problem - - -@attr(shard=9) -class CoursewareTest(UniqueCourseTest): - """ - Test courseware. - """ - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - - def setUp(self): - super(CoursewareTest, self).setUp() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - self.course_home_page = CourseHomePage(self.browser, self.course_id) - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # Install a course with sections/problems, tabs, updates, and handouts - self.course_fix = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - self.course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children( - XBlockFixtureDesc('problem', 'Test Problem 1') - ) - ), - XBlockFixtureDesc('chapter', 'Test Section 2').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 2').add_children( - XBlockFixtureDesc('problem', 'Test Problem 2') - ) - ) - ).install() - - # Auto-auth register for the course. - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - - def _goto_problem_page(self): - """ - Open problem page with assertion. - """ - self.courseware_page.visit() - self.problem_page = ProblemPage(self.browser) # pylint: disable=attribute-defined-outside-init - self.assertEqual(self.problem_page.problem_name, 'Test Problem 1') - - def test_courseware(self): - """ - Test courseware if recent visited subsection become unpublished. - """ - - # Visit problem page as a student. - self._goto_problem_page() - - # Logout and login as a staff user. - LogoutPage(self.browser).visit() - auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) - - # Visit course outline page in studio. - self.studio_course_outline.visit() - - # Set release date for subsection in future. - self.studio_course_outline.change_problem_release_date() - - # Logout and login as a student. - LogoutPage(self.browser).visit() - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - - # Visit courseware as a student. - self.courseware_page.visit() - # Problem name should be "Test Problem 2". - self.assertEqual(self.problem_page.problem_name, 'Test Problem 2') - - def test_course_tree_breadcrumb(self): - """ - Scenario: Correct course tree breadcrumb is shown. - - Given that I am a registered user - And I visit my courseware page - Then I should see correct course tree breadcrumb - """ - xblocks = self.course_fix.get_nested_xblocks(category="problem") - for index in range(1, len(xblocks) + 1): - test_section_title = u'Test Section {}'.format(index) - test_subsection_title = u'Test Subsection {}'.format(index) - test_unit_title = u'Test Problem {}'.format(index) - self.course_home_page.visit() - self.course_home_page.outline.go_to_section(test_section_title, test_subsection_title) - course_nav = self.courseware_page.nav - self.assertEqual(course_nav.breadcrumb_section_title, test_section_title) - self.assertEqual(course_nav.breadcrumb_subsection_title, test_subsection_title) - self.assertEqual(course_nav.breadcrumb_unit_title, test_unit_title) - - -class CoursewareMultipleVerticalsTestBase(UniqueCourseTest, EventsTestMixin): - """ - Base class with setup for testing courseware with multiple verticals - """ - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - - def setUp(self): - super(CoursewareMultipleVerticalsTestBase, self).setUp() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - self.course_home_page = CourseHomePage(self.browser, self.course_id) - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # Install a course with sections/problems, tabs, updates, and handouts - course_fix = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1,1').add_children( - XBlockFixtureDesc('problem', 'Test Problem 1', data='problem 1 dummy body'), - XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body"), - XBlockFixtureDesc('problem', 'Test Problem 2', data="problem 2 dummy body"), - XBlockFixtureDesc('html', 'html 2', data="html 2 dummy body"), - ), - XBlockFixtureDesc('sequential', 'Test Subsection 1,2').add_children( - XBlockFixtureDesc('problem', 'Test Problem 3', data='problem 3 dummy body'), - ), - XBlockFixtureDesc( - 'sequential', 'Test HIDDEN Subsection', metadata={'visible_to_staff_only': True} - ).add_children( - XBlockFixtureDesc('problem', 'Test HIDDEN Problem', data='hidden problem'), - ), - ), - XBlockFixtureDesc('chapter', 'Test Section 2').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 2,1').add_children( - XBlockFixtureDesc('problem', 'Test Problem 4', data='problem 4 dummy body'), - ), - ), - XBlockFixtureDesc('chapter', 'Test HIDDEN Section', metadata={'visible_to_staff_only': True}).add_children( - XBlockFixtureDesc('sequential', 'Test HIDDEN Subsection'), - ), - ).install() - - # Auto-auth register for the course. - AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, - course_id=self.course_id, staff=False).visit() - - -@attr(shard=9) -class CoursewareMultipleVerticalsTest(CoursewareMultipleVerticalsTestBase): - """ - Test courseware with multiple verticals - """ - - def test_navigation_buttons(self): - self.courseware_page.visit() - - # start in first section - self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 0, next_enabled=True, prev_enabled=False) - - # next takes us to next tab in sequential - self.courseware_page.click_next_button_on_top() - self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 1, next_enabled=True, prev_enabled=True) - - # go to last sequential position - self.courseware_page.go_to_sequential_position(4) - self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 3, next_enabled=True, prev_enabled=True) - - # next takes us to next sequential - self.courseware_page.click_next_button_on_bottom() - self.assert_navigation_state('Test Section 1', 'Test Subsection 1,2', 0, next_enabled=True, prev_enabled=True) - - # next takes us to next chapter - self.courseware_page.click_next_button_on_top() - self.assert_navigation_state('Test Section 2', 'Test Subsection 2,1', 0, next_enabled=False, prev_enabled=True) - - # previous takes us to previous chapter - self.courseware_page.click_previous_button_on_top() - self.assert_navigation_state('Test Section 1', 'Test Subsection 1,2', 0, next_enabled=True, prev_enabled=True) - - # previous takes us to last tab in previous sequential - self.courseware_page.click_previous_button_on_bottom() - self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 3, next_enabled=True, prev_enabled=True) - - # previous takes us to previous tab in sequential - self.courseware_page.click_previous_button_on_bottom() - self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 2, next_enabled=True, prev_enabled=True) - - # test UI events emitted by navigation - filter_sequence_ui_event = lambda event: event.get('name', '').startswith('edx.ui.lms.sequence.') - - sequence_ui_events = self.wait_for_events(event_filter=filter_sequence_ui_event, timeout=2) - legacy_events = [ev for ev in sequence_ui_events if ev['event_type'] in {'seq_next', 'seq_prev', 'seq_goto'}] - nonlegacy_events = [ev for ev in sequence_ui_events if ev not in legacy_events] - - self.assertTrue(all('old' in json.loads(ev['event']) for ev in legacy_events)) - self.assertTrue(all('new' in json.loads(ev['event']) for ev in legacy_events)) - self.assertFalse(any('old' in json.loads(ev['event']) for ev in nonlegacy_events)) - self.assertFalse(any('new' in json.loads(ev['event']) for ev in nonlegacy_events)) - - self.assert_events_match( - [ - { - 'event_type': 'seq_next', - 'event': { - 'old': 1, - 'new': 2, - 'current_tab': 1, - 'tab_count': 4, - 'widget_placement': 'top', - } - }, - { - 'event_type': 'seq_goto', - 'event': { - 'old': 2, - 'new': 4, - 'current_tab': 2, - 'target_tab': 4, - 'tab_count': 4, - 'widget_placement': 'top', - } - }, - { - 'event_type': 'edx.ui.lms.sequence.next_selected', - 'event': { - 'current_tab': 4, - 'tab_count': 4, - 'widget_placement': 'bottom', - } - }, - { - 'event_type': 'edx.ui.lms.sequence.next_selected', - 'event': { - 'current_tab': 1, - 'tab_count': 1, - 'widget_placement': 'top', - } - }, - { - 'event_type': 'edx.ui.lms.sequence.previous_selected', - 'event': { - 'current_tab': 1, - 'tab_count': 1, - 'widget_placement': 'top', - } - }, - { - 'event_type': 'edx.ui.lms.sequence.previous_selected', - 'event': { - 'current_tab': 1, - 'tab_count': 1, - 'widget_placement': 'bottom', - } - }, - { - 'event_type': 'seq_prev', - 'event': { - 'old': 4, - 'new': 3, - 'current_tab': 4, - 'tab_count': 4, - 'widget_placement': 'bottom', - } - }, - ], - sequence_ui_events - ) - - def assert_navigation_state( - self, section_title, subsection_title, subsection_position, next_enabled, prev_enabled - ): - """ - Verifies that the navigation state is as expected. - """ - self.assertTrue(self.courseware_page.nav.is_on_section(section_title, subsection_title)) - self.assertEqual(self.courseware_page.sequential_position, subsection_position) - self.assertEqual(self.courseware_page.is_next_button_enabled, next_enabled) - self.assertEqual(self.courseware_page.is_previous_button_enabled, prev_enabled) - - def test_tab_position(self): - # test that using the position in the url direct to correct tab in courseware - self.course_home_page.visit() - - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1,1') - subsection_url = self.browser.current_url - url_part_list = subsection_url.split('/') - - course_id = url_part_list[-5] - chapter_id = url_part_list[-3] - subsection_id = url_part_list[-2] - problem1_page = CoursewareSequentialTabPage( - self.browser, - course_id=course_id, - chapter=chapter_id, - subsection=subsection_id, - position=1 - ).visit() - self.assertIn('problem 1 dummy body', problem1_page.get_selected_tab_content()) - - html1_page = CoursewareSequentialTabPage( - self.browser, - course_id=course_id, - chapter=chapter_id, - subsection=subsection_id, - position=2 - ).visit() - self.assertIn('html 1 dummy body', html1_page.get_selected_tab_content()) - - problem2_page = CoursewareSequentialTabPage( - self.browser, - course_id=course_id, - chapter=chapter_id, - subsection=subsection_id, - position=3 - ).visit() - self.assertIn('problem 2 dummy body', problem2_page.get_selected_tab_content()) - - html2_page = CoursewareSequentialTabPage( - self.browser, - course_id=course_id, - chapter=chapter_id, - subsection=subsection_id, - position=4 - ).visit() - self.assertIn('html 2 dummy body', html2_page.get_selected_tab_content()) - - -@attr(shard=9) -class ProblemStateOnNavigationTest(UniqueCourseTest): - """ - Test courseware with problems in multiple verticals. - """ - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - - problem1_name = 'MULTIPLE CHOICE TEST PROBLEM 1' - problem2_name = 'MULTIPLE CHOICE TEST PROBLEM 2' - - def setUp(self): - super(ProblemStateOnNavigationTest, self).setUp() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - # Install a course with section, tabs and multiple choice problems. - course_fix = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1,1').add_children( - create_multiple_choice_problem(self.problem1_name), - create_multiple_choice_problem(self.problem2_name), - ), - ), - ).install() - - # Auto-auth register for the course. - AutoAuthPage( - self.browser, username=self.USERNAME, email=self.EMAIL, - course_id=self.course_id, staff=False - ).visit() - - self.courseware_page.visit() - self.problem_page = ProblemPage(self.browser) - - def go_to_tab_and_assert_problem(self, position, problem_name): - """ - Go to sequential tab and assert that we are on problem whose name is given as a parameter. - Args: - position: Position of the sequential tab - problem_name: Name of the problem - """ - self.courseware_page.go_to_sequential_position(position) - self.problem_page.wait_for_element_presence( - self.problem_page.CSS_PROBLEM_HEADER, - 'wait for problem header' - ) - self.assertEqual(self.problem_page.problem_name, problem_name) - - def test_perform_problem_submit_and_navigate(self): - """ - Scenario: - I go to sequential position 1 - Facing problem1, I select 'choice_1' - Then I click submit button - Then I go to sequential position 2 - Then I came back to sequential position 1 again - Facing problem1, I observe the problem1 content is not - outdated before and after sequence navigation - """ - # Go to sequential position 1 and assert that we are on problem 1. - self.go_to_tab_and_assert_problem(1, self.problem1_name) - - # Update problem 1's content state by clicking check button. - self.problem_page.click_choice('choice_choice_1') - self.problem_page.click_submit() - self.problem_page.wait_for_expected_status('label.choicegroup_incorrect', 'incorrect') - - # Save problem 1's content state as we're about to switch units in the sequence. - problem1_content_before_switch = self.problem_page.problem_content - before_meta = self.problem_page.problem_meta - - # Go to sequential position 2 and assert that we are on problem 2. - self.go_to_tab_and_assert_problem(2, self.problem2_name) - - # Come back to our original unit in the sequence and assert that the content hasn't changed. - self.go_to_tab_and_assert_problem(1, self.problem1_name) - problem1_content_after_coming_back = self.problem_page.problem_content - after_meta = self.problem_page.problem_meta - - self.assertEqual(problem1_content_before_switch, problem1_content_after_coming_back) - self.assertEqual(before_meta, after_meta) - - def test_perform_problem_save_and_navigate(self): - """ - Scenario: - I go to sequential position 1 - Facing problem1, I select 'choice_1' - Then I click save button - Then I go to sequential position 2 - Then I came back to sequential position 1 again - Facing problem1, I observe the problem1 content is not - outdated before and after sequence navigation - """ - # Go to sequential position 1 and assert that we are on problem 1. - self.go_to_tab_and_assert_problem(1, self.problem1_name) - - # Update problem 1's content state by clicking save button. - self.problem_page.click_choice('choice_choice_1') - self.problem_page.click_save() - self.problem_page.wait_for_save_notification() - - # Save problem 1's content state as we're about to switch units in the sequence. - problem1_content_before_switch = self.problem_page.problem_input_content - before_meta = self.problem_page.problem_meta - - # Go to sequential position 2 and assert that we are on problem 2. - self.go_to_tab_and_assert_problem(2, self.problem2_name) - - self.problem_page.wait_for_expected_status('span.unanswered', 'unanswered') - - # Come back to our original unit in the sequence and assert that the content hasn't changed. - self.go_to_tab_and_assert_problem(1, self.problem1_name) - problem1_content_after_coming_back = self.problem_page.problem_input_content - after_meta = self.problem_page.problem_meta - - self.assertIn(problem1_content_after_coming_back, problem1_content_before_switch) - self.assertEqual(before_meta, after_meta) - - -@attr(shard=9) -class SubsectionHiddenAfterDueDateTest(UniqueCourseTest): - """ - Tests the "hide after due date" setting for - subsections. - """ - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - - def setUp(self): - super(SubsectionHiddenAfterDueDateTest, self).setUp() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - self.logout_page = LogoutPage(self.browser) - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # Install a course with sections/problems, tabs, updates, and handouts - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children( - create_multiple_choice_problem('Test Problem 1') - ) - ) - ).install() - - self.progress_page = ProgressPage(self.browser, self.course_id) - self._setup_subsection() - - # Auto-auth register for the course. - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - - def _setup_subsection(self): - """ - Helper to set up a problem subsection as staff, then take - it as a student. - """ - self.logout_page.visit() - auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) - self.studio_course_outline.visit() - self.studio_course_outline.open_subsection_settings_dialog() - - self.studio_course_outline.select_visibility_tab() - self.studio_course_outline.make_subsection_hidden_after_due_date() - - self.logout_page.visit() - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - self.courseware_page.visit() - - self.logout_page.visit() - - def test_subsecton_hidden_after_due_date(self): - """ - Given that I am a staff member on the subsection settings section - And I select the advanced settings tab - When I Make the subsection hidden after its due date. - And I login as a student. - And visit the subsection in the courseware as a verified student. - Then I am able to see the subsection - And when I visit the progress page - Then I should be able to see my grade on the progress page - When I log in as staff - And I make the subsection due in the past so that the current date is past its due date - And I log in as a student - And I visit the subsection in the courseware - Then the subsection should be hidden with a message that its due date has passed - And when I visit the progress page - Then I should be able to see my grade on the progress page - """ - self.logout_page.visit() - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - self.courseware_page.visit() - self.assertFalse(self.courseware_page.content_hidden_past_due_date()) - - self.progress_page.visit() - self.assertEqual(self.progress_page.scores('Test Section 1', 'Test Subsection 1'), [(0, 1)]) - - self.logout_page.visit() - auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) - self.studio_course_outline.visit() - last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y") - self.studio_course_outline.change_problem_due_date(last_week) - - self.logout_page.visit() - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - self.courseware_page.visit() - self.assertTrue(self.courseware_page.content_hidden_past_due_date()) - - self.progress_page.visit() - self.assertEqual(self.progress_page.scores('Test Section 1', 'Test Subsection 1'), [(0, 1)]) - - -@attr(shard=9) -class CompletionTestCase(UniqueCourseTest, EventsTestMixin): - """ - Test the completion on view functionality. - """ - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - COMPLETION_BY_VIEWING_DELAY_MS = '1000' - - def setUp(self): - super(CompletionTestCase, self).setUp() - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # Install a course with sections/problems, tabs, updates, and handouts - course_fix = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - self.html_1_block = XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body") - self.problem_1_block = XBlockFixtureDesc( - 'problem', 'Test Problem 1', data='problem 1 dummy body' - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1,1').add_children( - XBlockFixtureDesc('vertical', 'Test Unit 1,1,1').add_children( - XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body"), - XBlockFixtureDesc( - 'html', 'html 2', - data=("html 2 dummy body" * 100) + "End", - ), - XBlockFixtureDesc('problem', 'Test Problem 1', data='problem 1 dummy body'), - ), - XBlockFixtureDesc('vertical', 'Test Unit 1,1,2').add_children( - XBlockFixtureDesc('html', 'html 1', data="html 1 dummy body"), - XBlockFixtureDesc('problem', 'Test Problem 1', data='problem 1 dummy body'), - ), - XBlockFixtureDesc('vertical', 'Test Unit 1,1,2').add_children( - self.html_1_block, - self.problem_1_block, - ), - ), - ), - ).install() - - # Auto-auth register for the course. - AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, - course_id=self.course_id, staff=False).visit() diff --git a/common/test/acceptance/tests/lms/test_lms_courseware_search.py b/common/test/acceptance/tests/lms/test_lms_courseware_search.py deleted file mode 100644 index 7610e26c9a..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_courseware_search.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Test courseware search -""" - - -import json - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.logout import LogoutPage -from common.test.acceptance.pages.common.utils import click_css -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage -from common.test.acceptance.pages.studio.container import ContainerPage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage -from common.test.acceptance.pages.studio.utils import add_html_component, type_in_codemirror -from common.test.acceptance.tests.helpers import UniqueCourseTest, remove_file - - -class CoursewareSearchTest(UniqueCourseTest): - """ - Test courseware search. - """ - USERNAME = 'STUDENT_TESTER' - EMAIL = 'student101@example.com' - - STAFF_USERNAME = "STAFF_TESTER" - STAFF_EMAIL = "staff101@example.com" - - HTML_CONTENT = """ - Someday I'll wish upon a star - And wake up where the clouds are far - Behind me. - Where troubles melt like lemon drops - Away above the chimney tops - That's where you'll find me. - """ - SEARCH_STRING = "chimney" - EDITED_CHAPTER_NAME = "Section 2 - edited" - EDITED_SEARCH_STRING = "edited" - - TEST_INDEX_FILENAME = "test_root/index_file.dat" - shard = 5 - - def setUp(self): - """ - Create search page and course content to search - """ - # create test file in which index for this test will live - with open(self.TEST_INDEX_FILENAME, "w+") as index_file: - json.dump({}, index_file) - self.addCleanup(remove_file, self.TEST_INDEX_FILENAME) - - super(CoursewareSearchTest, self).setUp() - - self.course_home_page = CourseHomePage(self.browser, self.course_id) - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Section 1').add_children( - XBlockFixtureDesc('sequential', 'Subsection 1') - ) - ).add_children( - XBlockFixtureDesc('chapter', 'Section 2').add_children( - XBlockFixtureDesc('sequential', 'Subsection 2') - ) - ).install() - - def _auto_auth(self, username, email, staff): - """ - Logout and login with given credentials. - """ - LogoutPage(self.browser).visit() - AutoAuthPage(self.browser, username=username, email=email, - course_id=self.course_id, staff=staff).visit() - - def _studio_publish_content(self, section_index): - """ - Publish content on studio course page under specified section - """ - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) - self.studio_course_outline.visit() - subsection = self.studio_course_outline.section_at(section_index).subsection_at(0) - subsection.expand_subsection() - unit = subsection.unit_at(0) - unit.publish() - - def _studio_edit_chapter_name(self, section_index): - """ - Edit chapter name on studio course page under specified section - """ - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) - self.studio_course_outline.visit() - section = self.studio_course_outline.section_at(section_index) - section.change_name(self.EDITED_CHAPTER_NAME) - - def _studio_add_content(self, section_index): - """ - Add content on studio course page under specified section - """ - - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) - # create a unit in course outline - self.studio_course_outline.visit() - subsection = self.studio_course_outline.section_at(section_index).subsection_at(0) - subsection.expand_subsection() - subsection.add_unit() - - # got to unit and create an HTML component and save (not publish) - unit_page = ContainerPage(self.browser, None) - unit_page.wait_for_page() - add_html_component(unit_page, 0) - unit_page.wait_for_element_presence('.edit-button', 'Edit button is visible') - click_css(unit_page, '.edit-button', 0, require_notification=False) - unit_page.wait_for_element_visibility('.modal-editor', 'Modal editor is visible') - type_in_codemirror(unit_page, 0, self.HTML_CONTENT) - click_css(unit_page, '.action-save', 0) - - def _studio_reindex(self): - """ - Reindex course content on studio course page - """ - - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) - self.studio_course_outline.visit() - self.studio_course_outline.start_reindex() - self.studio_course_outline.wait_for_ajax() - - def _search_for_content(self, search_term): - """ - Login and search for specific content - - Arguments: - search_term - term to be searched for - - Returns: - (bool) True if search term is found in resulting content; False if not found - """ - self._auto_auth(self.USERNAME, self.EMAIL, False) - self.course_home_page.visit() - course_search_results_page = self.course_home_page.search_for_term(search_term) - if len(course_search_results_page.search_results.html) > 0: - search_string = course_search_results_page.search_results.html[0] - else: - search_string = "" - return search_term in search_string - - # TODO: TNL-6546: Remove usages of sidebar search - def _search_for_content_in_sidebar(self, search_term, perform_auto_auth=True): - """ - Login and search for specific content in the legacy sidebar search - Arguments: - search_term - term to be searched for - perform_auto_auth - if False, skip auto_auth call. - Returns: - (bool) True if search term is found in resulting content; False if not found - """ - if perform_auto_auth: - self._auto_auth(self.USERNAME, self.EMAIL, False) - self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id) - self.courseware_search_page.visit() - self.courseware_search_page.search_for_term(search_term) - return search_term in self.courseware_search_page.search_results.html[0] - - def test_search(self): - """ - Make sure that you can search for something. - """ - - # Create content in studio without publishing. - self._studio_add_content(0) - - # Do a search, there should be no results shown. - self.assertFalse(self._search_for_content(self.SEARCH_STRING)) - - # Do a search in the legacy sidebar, there should be no results shown. - self.assertFalse(self._search_for_content_in_sidebar(self.SEARCH_STRING, False)) - - # Publish in studio to trigger indexing. - self._studio_publish_content(0) - - # Do the search again, this time we expect results. - self.assertTrue(self._search_for_content(self.SEARCH_STRING)) - - # Do the search again in the legacy sidebar, this time we expect results. - self.assertTrue(self._search_for_content_in_sidebar(self.SEARCH_STRING, False)) diff --git a/common/test/acceptance/tests/lms/test_lms_dashboard.py b/common/test/acceptance/tests/lms/test_lms_dashboard.py index 5982148a65..2f254af733 100644 --- a/common/test/acceptance/tests/lms/test_lms_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_dashboard.py @@ -3,64 +3,17 @@ End-to-end tests for the main LMS Dashboard (aka, Student Dashboard). """ - -import datetime -import re import six -from six.moves.urllib.parse import unquote + from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.course_home import CourseHomePage from common.test.acceptance.pages.lms.dashboard import DashboardPage -from common.test.acceptance.pages.lms.problem import ProblemPage -from common.test.acceptance.pages.lms.staff_view import StaffPreviewPage from common.test.acceptance.tests.helpers import UniqueCourseTest, generate_course_key DEFAULT_SHORT_DATE_FORMAT = u'{dt:%b} {dt.day}, {dt.year}' TEST_DATE_FORMAT = u'{dt:%b} {dt.day}, {dt.year} {dt.hour:02}:{dt.minute:02}' -class BaseLmsDashboardTest(UniqueCourseTest): - """ Base test suite for the LMS Student Dashboard """ - - def setUp(self): - """ - Initializes the components (page objects, courses, users) for this test suite - """ - # Some parameters are provided by the parent setUp() routine, such as the following: - # self.course_id, self.course_info, self.unique_id - super(BaseLmsDashboardTest, self).setUp() - - # Load page objects for use by the tests - self.dashboard_page = DashboardPage(self.browser) - - # Configure some aspects of the test course and install the settings into the course - self.course_fixture = CourseFixture( - self.course_info["org"], - self.course_info["number"], - self.course_info["run"], - self.course_info["display_name"], - ) - self.course_fixture.add_advanced_settings({ - u"social_sharing_url": {u"value": "http://custom/course/url"} - }) - self.course_fixture.install() - - self.username = "test_{uuid}".format(uuid=self.unique_id[0:6]) - self.email = "{user}@example.com".format(user=self.username) - - # Create the test user, register them for the course, and authenticate - AutoAuthPage( - self.browser, - username=self.username, - email=self.email, - course_id=self.course_id - ).visit() - - # Navigate the authenticated, enrolled user to the dashboard page and get testing! - self.dashboard_page.visit() - - class BaseLmsDashboardTestMultiple(UniqueCourseTest): """ Base test suite for the LMS Student Dashboard with Multiple Courses""" @@ -162,272 +115,6 @@ class BaseLmsDashboardTestMultiple(UniqueCourseTest): self.dashboard_page.visit() -class LmsDashboardPageTest(BaseLmsDashboardTest): - """ Test suite for the LMS Student Dashboard page """ - shard = 9 - - def setUp(self): - super(LmsDashboardPageTest, self).setUp() - - # now datetime for usage in tests - self.now = datetime.datetime.now() - - def test_dashboard_course_listings(self): - """ - Perform a general validation of the course listings section - """ - course_listings = self.dashboard_page.get_course_listings() - self.assertEqual(len(course_listings), 1) - - def test_dashboard_social_sharing_feature(self): - """ - Validate the behavior of the social sharing feature - """ - twitter_widget = self.dashboard_page.get_course_social_sharing_widget('twitter') - twitter_url = ("https://twitter.com/intent/tweet?text=Testing+feature%3A%20http%3A%2F%2Fcustom%2Fcourse%2Furl" - "%3Futm_campaign%3Dsocial-sharing-db%26utm_medium%3Dsocial%26utm_source%3Dtwitter") - self.assertEqual(twitter_widget.attrs('title')[0], 'Share on Twitter') - self.assertEqual(twitter_widget.attrs('data-tooltip')[0], 'Share on Twitter') - self.assertEqual(twitter_widget.attrs('target')[0], '_blank') - self._assert_social_url(twitter_widget.attrs('href')[0], unquote(twitter_url), r"\'(.*?)\'\,") - self._assert_social_url(twitter_widget.attrs('onclick')[0], unquote(twitter_url), r"\'(.*?)\'\,") - - facebook_widget = self.dashboard_page.get_course_social_sharing_widget('facebook') - facebook_url = ("https://www.facebook.com/sharer/sharer.php?u=http%3A%2F%2Fcustom%2Fcourse%2Furl%3F" - "utm_campaign%3Dsocial-sharing-db%26utm_medium%3Dsocial%26utm_source%3Dfacebook&" - "quote=I%27m+taking+Test+Course") - self.assertEqual(facebook_widget.attrs('title')[0], 'Share on Facebook') - self.assertEqual(facebook_widget.attrs('data-tooltip')[0], 'Share on Facebook') - self.assertEqual(facebook_widget.attrs('target')[0], '_blank') - self._assert_social_url(facebook_widget.attrs('onclick')[0], unquote(facebook_url), r"\'(.*?);") - self._assert_social_url(facebook_widget.attrs('href')[0], unquote(facebook_url), r"^(.*)\;") - - def _assert_social_url(self, url, expected_url, pattern): - """ - will remove byte characters from specific query parameter - """ - url = unquote(url) - social_url_search = re.search(pattern, url) - url_split = (social_url_search.group(1) if social_url_search else url).split('?') - query_parameters = url_split[2].split('&') - urls = url_split[:2] + query_parameters - for query_parameter in urls: - self.assertIn(query_parameter.strip("'"), expected_url) - - def test_ended_course_date(self): - """ - Scenario: - Course Date should have the format 'Ended - Sep 23, 2015' - if the course on student dashboard has ended. - As a Student, - Given that I have enrolled to a course - And the course has ended in the past - When I visit dashboard page - Then the course date should have the following format "Ended - %b %d, %Y" e.g. "Ended - Sep 23, 2015" - """ - course_start_date = datetime.datetime(1970, 1, 1) - course_end_date = self.now - datetime.timedelta(days=90) - - self.course_fixture.add_course_details({ - 'start_date': course_start_date, - 'end_date': course_end_date - }) - self.course_fixture.configure_course() - - end_date = DEFAULT_SHORT_DATE_FORMAT.format(dt=course_end_date) - expected_course_date = u"Ended - {end_date}".format(end_date=end_date) - - # reload the page for changes to course date changes to appear in dashboard - self.dashboard_page.visit() - - course_date = self.dashboard_page.get_course_date() - - # Test that proper course date with 'ended' message is displayed if a course has already ended - self.assertEqual(course_date, expected_course_date) - - def test_running_course_date(self): - """ - Scenario: - Course Date should have the format 'Started - Sep 23, 2015' - if the course on student dashboard is running. - As a Student, - Given that I have enrolled to a course - And the course has started - And the course is in progress - When I visit dashboard page - Then the course date should have the following format "Started - %b %d, %Y" e.g. "Started - Sep 23, 2015" - """ - course_start_date = datetime.datetime(1970, 1, 1) - course_end_date = self.now + datetime.timedelta(days=90) - - self.course_fixture.add_course_details({ - 'start_date': course_start_date, - 'end_date': course_end_date - }) - self.course_fixture.configure_course() - - start_date = DEFAULT_SHORT_DATE_FORMAT.format(dt=course_start_date) - expected_course_date = u"Started - {start_date}".format(start_date=start_date) - - # reload the page for changes to course date changes to appear in dashboard - self.dashboard_page.visit() - - course_date = self.dashboard_page.get_course_date() - - # Test that proper course date with 'started' message is displayed if a course is in running state - self.assertEqual(course_date, expected_course_date) - - def test_future_course_date(self): - """ - Scenario: - Course Date should have the format 'Starts - Sep 23, 2015' - if the course on student dashboard starts in future. - As a Student, - Given that I have enrolled to a course - And the course starts in future - And the course does not start within 5 days - When I visit dashboard page - Then the course date should have the following format "Starts - %b %d, %Y" e.g. "Starts - Sep 23, 2015" - """ - course_start_date = self.now + datetime.timedelta(days=30) - course_end_date = self.now + datetime.timedelta(days=365) - - self.course_fixture.add_course_details({ - 'start_date': course_start_date, - 'end_date': course_end_date - }) - self.course_fixture.configure_course() - - start_date = DEFAULT_SHORT_DATE_FORMAT.format(dt=course_start_date) - expected_course_date = u"Starts - {start_date}".format(start_date=start_date) - - # reload the page for changes to course date changes to appear in dashboard - self.dashboard_page.visit() - - course_date = self.dashboard_page.get_course_date() - - # Test that proper course date with 'starts' message is displayed if a course is about to start in future, - # and course does not start within 5 days - self.assertEqual(course_date, expected_course_date) - - def test_near_future_course_date(self): - """ - Scenario: - Course Date should have the format 'Starts - Wednesday at 5am UTC' - if the course on student dashboard starts within 5 days. - As a Student, - Given that I have enrolled to a course - And the course starts within 5 days - When I visit dashboard page - Then the course date should have the following format "Starts - %A at %-I%P UTC" - e.g. "Starts - Wednesday at 5am UTC" - """ - course_start_date = self.now + datetime.timedelta(days=2) - course_end_date = self.now + datetime.timedelta(days=365) - - self.course_fixture.add_course_details({ - 'start_date': course_start_date, - 'end_date': course_end_date - }) - self.course_fixture.configure_course() - - start_date = TEST_DATE_FORMAT.format(dt=course_start_date) - expected_course_date = u"Starts - {start_date} UTC".format(start_date=start_date) - - # reload the page for changes to course date changes to appear in dashboard - self.dashboard_page.visit() - - course_date = self.dashboard_page.get_course_date() - - # Test that proper course date with 'starts' message is displayed if a course is about to start in future, - # and course starts within 5 days - self.assertEqual(course_date, expected_course_date) - - def test_advertised_start_date(self): - """ - Scenario: - Course Date should be advertised start date - if the course on student dashboard has `Course Advertised Start` set. - - As a Student, - Given that I have enrolled to a course - And the course has `Course Advertised Start` set. - When I visit dashboard page - Then the advertised start date should be displayed rather course start date" - """ - course_start_date = self.now + datetime.timedelta(days=2) - course_advertised_start = "Winter 2018" - - self.course_fixture.add_course_details({ - 'start_date': course_start_date, - }) - self.course_fixture.configure_course() - - self.course_fixture.add_advanced_settings({ - u"advertised_start": {u"value": course_advertised_start} - }) - self.course_fixture._add_advanced_settings() - - expected_course_date = u"Starts - {start_date}".format(start_date=course_advertised_start) - - self.dashboard_page.visit() - course_date = self.dashboard_page.get_course_date() - - self.assertEqual(course_date, expected_course_date) - - def test_profile_img_alt_empty(self): - """ - Validate value of profile image alt attribue is null - """ - profile_img = self.dashboard_page.get_profile_img() - self.assertEqual(profile_img.attrs('alt')[0], '') - - -class LmsDashboardCourseUnEnrollDialogMessageTest(BaseLmsDashboardTestMultiple): - """ - Class to test lms student dashboard unenroll dialog messages. - """ - shard = 23 - - def test_audit_course_run_unenroll_dialog_msg(self): - """ - Validate unenroll dialog message when user clicks unenroll button for a audit course - """ - - self.dashboard_page.visit() - dialog_message = self.dashboard_page.view_course_unenroll_dialog_message(str(self.course_keys['A'])) - course_number = self.courses['A']['number'] - course_name = self.courses['A']['display_name'] - - expected_track_message = u'Are you sure you want to unenroll from' + \ - u' ' + course_name + u'' + \ - u' (' + course_number + u')?' - - self.assertEqual(dialog_message['track-info'][0], expected_track_message) - - def test_verified_course_run_unenroll_dialog_msg(self): - """ - Validate unenroll dialog message when user clicks unenroll button for a verified course passed refund - deadline - """ - - self.dashboard_page.visit() - dialog_message = self.dashboard_page.view_course_unenroll_dialog_message(str(self.course_keys['B'])) - course_number = self.courses['B']['number'] - course_name = self.courses['B']['display_name'] - cert_long_name = self.courses['B']['cert_name_long'] - - expected_track_message = u'Are you sure you want to unenroll from the verified' + \ - u' ' + cert_long_name + u'' + \ - u' track of ' + course_name + u'' + \ - u' (' + course_number + u')?' - - expected_refund_message = u'The refund deadline for this course has passed,so you will not receive a refund.' - - self.assertEqual(dialog_message['track-info'][0], expected_track_message) - self.assertEqual(dialog_message['refund-info'][0], expected_refund_message) - - class LmsDashboardA11yTest(BaseLmsDashboardTestMultiple): """ Class to test lms student dashboard accessibility. @@ -450,49 +137,3 @@ class LmsDashboardA11yTest(BaseLmsDashboardTestMultiple): course_listings = self.dashboard_page.get_courses() self.assertEqual(len(course_listings), 3) self.dashboard_page.a11y_audit.check_for_accessibility_errors() - - -class TestMasqueradeAndSwitchCourse(BaseLmsDashboardTestMultiple): - """ - Class to test lms dashboard accessibility of courses when masquerading as learner. - """ - - def test_masquerade_and_switch_course(self): - """ - Scenario: - Staff user should be able to access other courses after - masquerading as student in one course - - As Staff user, Select a course - When I click to change view from Staff to Learner - Then the first subsection from course outline should be visible as Learner - When I click to select a different course - Then the first subsection from new course outline should be visible as Staff - """ - AutoAuthPage( - self.browser, - username=self.username, - email=self.email, - staff=True - ).visit() - self.dashboard_page.visit() - - section_title = 'Test Section 1' - subsection_title = 'Test Subsection 1,1' - course_page = CourseHomePage(self.browser, str(self.course_keys['A'])) - course_page.visit() - - problem_name = u'Test Problem 1' - - staff_page = StaffPreviewPage(self.browser) - staff_page.set_staff_view_mode('Learner') - - course_page.outline.go_to_section(section_title, subsection_title) - self.assertEqual(staff_page.staff_view_mode, 'Learner') - self.assertEqual(ProblemPage(self.browser).problem_name, problem_name) - - course_page.course_id = str(self.course_keys['B']) - course_page.visit() - course_page.outline.go_to_section(section_title, subsection_title) - self.assertNotEqual(staff_page.staff_view_mode, 'Learner') - self.assertEqual(ProblemPage(self.browser).problem_name, problem_name) diff --git a/common/test/acceptance/tests/lms/test_lms_entrance_exams.py b/common/test/acceptance/tests/lms/test_lms_entrance_exams.py deleted file mode 100644 index f23e01661b..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_entrance_exams.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Bok choy acceptance tests for Entrance exams in the LMS -""" - - -from textwrap import dedent - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.tests.helpers import UniqueCourseTest - - -class EntranceExamTest(UniqueCourseTest): - """ - Base class for tests of Entrance Exams in the LMS. - """ - USERNAME = "joe_student" - EMAIL = "joe@example.com" - - def setUp(self): - super(EntranceExamTest, self).setUp() - - self.xqueue_grade_response = None - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - # Install a course with a hierarchy and problems - course_fixture = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'], - settings={ - 'entrance_exam_enabled': 'true', - 'entrance_exam_minimum_score_pct': '50' - } - ) - - problem = self.get_problem() - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children(problem) - ) - ).install() - - entrance_exam_subsection = None - outline = course_fixture.studio_course_outline_as_json - for child in outline['child_info']['children']: - if child.get('display_name') == "Entrance Exam": - entrance_exam_subsection = child['child_info']['children'][0] - - if entrance_exam_subsection: - course_fixture.create_xblock(entrance_exam_subsection['id'], problem) - - # Auto-auth register for the course. - AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, - course_id=self.course_id, staff=False).visit() - - def get_problem(self): - """ Subclasses should override this to complete the fixture """ - raise NotImplementedError() - - -class EntranceExamPassTest(EntranceExamTest): - """ - Tests the scenario when a student passes entrance exam. - """ - - def get_problem(self): - """ - Create a multiple choice problem - """ - xml = dedent(""" - - - - - 324 metersAntenna is 24 meters high - 300 meters - 224 meters - 400 meters - - - - """) - return XBlockFixtureDesc('problem', 'HEIGHT OF EIFFEL TOWER', data=xml) diff --git a/common/test/acceptance/tests/lms/test_lms_gating.py b/common/test/acceptance/tests/lms/test_lms_gating.py deleted file mode 100644 index 512ba2f675..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_gating.py +++ /dev/null @@ -1,279 +0,0 @@ -# -*- coding: utf-8 -*- -""" -End-to-end tests for the gating feature. -""" - - -from textwrap import dedent - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.logout import LogoutPage -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.problem import ProblemPage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage -from common.test.acceptance.tests.helpers import UniqueCourseTest - - -class GatingTest(UniqueCourseTest): - """ - Test gating feature in LMS. - """ - STAFF_USERNAME = "STAFF_TESTER" - STAFF_EMAIL = "staff101@example.com" - - STUDENT_USERNAME = "STUDENT_TESTER" - STUDENT_EMAIL = "student101@example.com" - - shard = 23 - - def setUp(self): - super(GatingTest, self).setUp() - - self.logout_page = LogoutPage(self.browser) - self.course_home_page = CourseHomePage(self.browser, self.course_id) - self.courseware_page = CoursewarePage(self.browser, self.course_id) - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - xml = dedent(""" - -

What is height of eiffel tower without the antenna?.

- - - 324 metersAntenna is 24 meters high - 300 meters - 224 meters - 400 meters - - -
- """) - self.problem1 = XBlockFixtureDesc('problem', 'HEIGHT OF EIFFEL TOWER', data=xml) - - # Install a course with sections/problems - course_fixture = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - course_fixture.add_advanced_settings({ - "enable_subsection_gating": {"value": "true"}, 'enable_proctored_exams': {"value": "true"} - }) - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children( - self.problem1 - ), - XBlockFixtureDesc('sequential', 'Test Subsection 2').add_children( - XBlockFixtureDesc('problem', 'Test Problem 2') - ), - XBlockFixtureDesc('sequential', 'Test Subsection 3').add_children( - XBlockFixtureDesc('problem', 'Test Problem 3') - ), - - ) - ).install() - - def _auto_auth(self, username, email, staff): - """ - Logout and login with given credentials. - """ - self.logout_page.visit() - AutoAuthPage(self.browser, username=username, email=email, - course_id=self.course_id, staff=staff).visit() - - def _setup_prereq(self): - """ - Make the first subsection a prerequisite - """ - # Login as staff - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) - - # Make the first subsection a prerequisite - self.studio_course_outline.visit() - self.studio_course_outline.open_subsection_settings_dialog(0) - self.studio_course_outline.select_advanced_tab(desired_item='gated_content') - self.studio_course_outline.make_gating_prerequisite() - - def _setup_gated_subsection(self, subsection_index=1): - """ - Gate the given indexed subsection on the first subsection - """ - # Login as staff - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) - - # Gate the second subsection based on the score achieved in the first subsection - self.studio_course_outline.visit() - self.studio_course_outline.open_subsection_settings_dialog(subsection_index) - self.studio_course_outline.select_advanced_tab(desired_item='gated_content') - self.studio_course_outline.add_prerequisite_to_subsection("80", "") - - def _fulfill_prerequisite(self): - """ - Fulfill the prerequisite needed to see gated content - """ - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.wait_for_page().problem_name, 'HEIGHT OF EIFFEL TOWER') - problem_page.click_choice('choice_1') - problem_page.click_submit() - - def test_subsection_gating_in_studio(self): - """ - Given that I am a staff member - When I visit the course outline page in studio. - And open the subsection edit dialog - Then I can view all settings related to Gating - And update those settings to gate a subsection - """ - self._setup_prereq() - - # Assert settings are displayed correctly for a prerequisite subsection - self.studio_course_outline.visit() - self.studio_course_outline.open_subsection_settings_dialog(0) - self.studio_course_outline.select_advanced_tab(desired_item='gated_content') - self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible()) - self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_checked()) - self.assertFalse(self.studio_course_outline.gating_prerequisites_dropdown_is_visible()) - self.assertFalse(self.studio_course_outline.gating_prerequisite_min_score_is_visible()) - - self._setup_gated_subsection() - - # Assert settings are displayed correctly for a gated subsection - self.studio_course_outline.visit() - self.studio_course_outline.open_subsection_settings_dialog(1) - self.studio_course_outline.select_advanced_tab(desired_item='gated_content') - self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible()) - self.assertTrue(self.studio_course_outline.gating_prerequisites_dropdown_is_visible()) - self.assertTrue(self.studio_course_outline.gating_prerequisite_min_score_is_visible()) - - def test_gated_subsection_in_lms_for_student(self): - """ - Given that I am a student - When I visit the LMS Courseware - Then I can see a gated subsection - The gated subsection should have a lock icon - and be in the format: " (Prerequisite Required)" - When I fulfill the gating Prerequisite - Then I can see the gated subsection - Now the gated subsection should have an unlock icon - and screen readers should read the section as: " Unlocked" - """ - self._setup_prereq() - self._setup_gated_subsection() - - self._auto_auth(self.STUDENT_USERNAME, self.STUDENT_EMAIL, False) - - self.course_home_page.visit() - self.assertEqual(self.course_home_page.outline.num_subsections, 3) - - # Fulfill prerequisite and verify that gated subsection is shown - self.courseware_page.visit() - self._fulfill_prerequisite() - self.course_home_page.visit() - self.assertEqual(self.course_home_page.outline.num_subsections, 3) - - def test_gated_subsection_in_lms_for_staff(self): - """ - Given that I am a staff member - When I visit the LMS Courseware - Then I can see all gated subsections - Displayed along with notification banners - Then if I masquerade as a student - Then I can see a gated subsection - The gated subsection should have a lock icon - and be in the format: " (Prerequisite Required)" - """ - self._setup_prereq() - self._setup_gated_subsection() - - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) - - self.course_home_page.visit() - self.assertEqual(self.course_home_page.preview.staff_view_mode, 'Staff') - self.assertEqual(self.course_home_page.outline.num_subsections, 3) - - # Click on gated section and check for banner - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 2') - self.courseware_page.wait_for_page() - self.assertTrue(self.courseware_page.has_banner()) - - self.course_home_page.visit() - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1') - self.courseware_page.wait_for_page() - - self.course_home_page.visit() - self.course_home_page.preview.set_staff_view_mode('Learner') - self.course_home_page.wait_for_page() - self.assertEqual(self.course_home_page.outline.num_subsections, 3) - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1') - self.courseware_page.wait_for_page() - # banner displayed informing section is a prereq - self.assertTrue(self.courseware_page.has_banner()) - - def test_gated_banner_before_special_exam(self): - """ - When a subsection with a prereq is a special - exam, show the gating banner before starting - the special exam. - - Setup the course with a subsection having pre-req - Subsection with pre-req is a special exam - Go the LMS course outline page - Click the special exam subsection - The gated banner asking for completing - prereqs should be visible - Go to the required subsection - Fulfill the requirements - Visit the special exam subsection again - The gated banner is not visible anymore - and user can start the special exam - """ - - self._setup_prereq() - - # Gating subsection 1 and making it a timed exam - self._setup_gated_subsection() - self.studio_course_outline.open_subsection_settings_dialog(1) - self.studio_course_outline.select_advanced_tab() - self.studio_course_outline.make_exam_timed() - - # Gating subsection 2 and making it a proctored exam - self._setup_gated_subsection(2) - self.studio_course_outline.open_subsection_settings_dialog(2) - self.studio_course_outline.select_advanced_tab() - self.studio_course_outline.make_exam_proctored() - - self._auto_auth(self.STUDENT_USERNAME, self.STUDENT_EMAIL, False) - self.course_home_page.visit() - - # Test gating banner before starting timed exam - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 2') - self.assertTrue(self.courseware_page.is_gating_banner_visible()) - - # Test gating banner before proctored exams - self.course_home_page.visit() - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 3') - self.assertTrue(self.courseware_page.is_gating_banner_visible()) - - # Fulfill requirements - self.course_home_page.visit() - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1') - self._fulfill_prerequisite() - - # Banner is not visible anymore on timed exam sub-section - self.course_home_page.visit() - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 2') - self.assertFalse(self.courseware_page.is_gating_banner_visible()) - - # Banner is not visible on proctored exam subsection - self.course_home_page.visit() - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 3') - self.assertFalse(self.courseware_page.is_gating_banner_visible()) diff --git a/common/test/acceptance/tests/lms/test_lms_help.py b/common/test/acceptance/tests/lms/test_lms_help.py deleted file mode 100644 index e88837d3da..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_help.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Test Help links in LMS -""" - - -from common.test.acceptance.fixtures.course import CourseFixture -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage -from common.test.acceptance.tests.discussion.helpers import CohortTestMixin -from common.test.acceptance.tests.helpers import ( - assert_opened_help_link_is_correct, - click_and_wait_for_window, - url_for_help -) -from common.test.acceptance.tests.lms.test_lms_instructor_dashboard import BaseInstructorDashboardTest -from common.test.acceptance.tests.studio.base_studio_test import ContainerBase -from openedx.core.release import skip_unless_master - -# @skip_unless_master is used throughout this file because on named release -# branches, most work happens leading up to the first release on the branch, and -# that is before the docs have been published. Tests that check readthedocs for -# the right doc page will fail during this time, and it's just a big -# distraction. Also, if we bork the docs, it's not the end of the world, and we -# can fix it easily, so this is a good tradeoff. - - -@skip_unless_master # See note at the top of the file. -class TestCohortHelp(ContainerBase, CohortTestMixin): - """ - Tests help links in Cohort page - """ - shard = 2 - - def setUp(self, is_staff=True): - super(TestCohortHelp, self).setUp(is_staff=is_staff) - self.enable_cohorting(self.course_fixture) - self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - self.instructor_dashboard_page.visit() - self.cohort_management = self.instructor_dashboard_page.select_cohort_management() - - def verify_help_link(self, href): - """ - Verifies that help link is correct - Arguments: - href (str): Help url - """ - help_element = self.cohort_management.get_cohort_help_element() - self.assertEqual(help_element.text, "What does this mean?") - click_and_wait_for_window(self, help_element) - assert_opened_help_link_is_correct(self, href) - - def test_manual_cohort_help(self): - """ - Scenario: Help in 'What does it mean?' is correct when we create cohort manually. - Given that I am at 'Cohort' tab of LMS instructor dashboard - And I check 'Enable Cohorts' - And I add cohort name it, choose Manual for Cohort Assignment Method and - No content group for Associated Content Group and save the cohort - Then you see the UI text "Learners are added to this cohort only when..." - followed by "What does this mean" link. - And I click "What does this mean" link then help link should end with - course_features/cohorts/cohort_config.html#assign-learners-to-cohorts-manually - """ - self.cohort_management.add_cohort('cohort_name') - - href = url_for_help( - 'course_author', - '/course_features/cohorts/cohort_config.html#assign-learners-to-cohorts-manually', - ) - self.verify_help_link(href) - - def test_automatic_cohort_help(self): - """ - Scenario: Help in 'What does it mean?' is correct when we create cohort automatically. - Given that I am at 'Cohort' tab of LMS instructor dashboard - And I check 'Enable Cohorts' - And I add cohort name it, choose Automatic for Cohort Assignment Method and - No content group for Associated Content Group and save the cohort - Then you see the UI text "Learners are added to this cohort automatically" - followed by "What does this mean" link. - And I click "What does this mean" link then help link should end with - course_features/cohorts/cohorts_overview.html#all-automated-assignment - """ - - self.cohort_management.add_cohort('cohort_name', assignment_type='random') - - href = url_for_help( - 'course_author', - '/course_features/cohorts/cohorts_overview.html#all-automated-assignment', - ) - self.verify_help_link(href) diff --git a/common/test/acceptance/tests/lms/test_lms_index.py b/common/test/acceptance/tests/lms/test_lms_index.py deleted file mode 100644 index dc15d9eb45..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_index.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -""" -End-to-end tests for the LMS Index page (aka, Home page). Note that this is different than -what students see @ edx.org because we redirect requests to a separate web application. -""" - - -import datetime - -from common.test.acceptance.pages.lms.index import IndexPage -from common.test.acceptance.tests.helpers import AcceptanceTest - - -class BaseLmsIndexTest(AcceptanceTest): - """ Base test suite for the LMS Index (Home) page """ - - def setUp(self): - """ - Initializes the components (page objects, courses, users) for this test suite - """ - # Some state is constructed by the parent setUp() routine - super(BaseLmsIndexTest, self).setUp() - - # Load page objects for use by the tests - self.page = IndexPage(self.browser) - - # Navigate to the index page and get testing! - self.page.visit() - - -class LmsIndexPageTest(BaseLmsIndexTest): - """ Test suite for the LMS Index (Home) page """ - shard = 2 - - def setUp(self): - super(LmsIndexPageTest, self).setUp() - - # Useful to capture the current datetime for our tests - self.now = datetime.datetime.now() - - def test_index_basic_request(self): - """ - Perform a general validation of the index page, renders normally, no exceptions raised, etc. - """ - self.assertTrue(self.page.banner_element.visible) - expected_links = [u'About', u'Blog', u'News', u'Help Center', u'Contact', u'Careers', u'Donate'] - self.assertEqual(self.page.footer_links, expected_links) - - def test_intro_video_hidden_by_default(self): - """ - Confirm that the intro video is not displayed when using the default configuration - """ - # Ensure the introduction video element is not shown - self.assertFalse(self.page.intro_video_element.visible) - - # Still need to figure out how to swap platform settings in the context of a bok choy test - # but we can at least prevent accidental exposure with these validations going forward - # Note: 'present' is a DOM check, whereas 'visible' is an actual browser/screen check - self.assertFalse(self.page.video_modal_element.present) - self.assertFalse(self.page.video_modal_element.visible) diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py index 6f0601c8ea..721d7ce82d 100644 --- a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py @@ -5,31 +5,18 @@ End-to-end tests for the LMS Instructor Dashboard. import ddt -from bok_choy.promise import EmptyPromise -from six.moves import range from common.test.acceptance.fixtures.certificates import CertificateConfigFixture -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc +from common.test.acceptance.fixtures.course import CourseFixture from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.logout import LogoutPage -from common.test.acceptance.pages.common.utils import enroll_user_track -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.create_mode import ModeCreationPage from common.test.acceptance.pages.lms.dashboard import DashboardPage from common.test.acceptance.pages.lms.instructor_dashboard import ( InstructorDashboardPage, - StudentAdminPage, - StudentSpecificAdmin ) -from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage -from common.test.acceptance.pages.lms.problem import ProblemPage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage from common.test.acceptance.tests.helpers import ( EventsTestMixin, UniqueCourseTest, - create_multiple_choice_problem, disable_animations, - get_modal_alert ) from openedx.core.lib.tests import attr @@ -101,11 +88,6 @@ class BulkEmailTest(BaseInstructorDashboardTest): instructor_dashboard_page = self.visit_instructor_dashboard() self.send_email_page = instructor_dashboard_page.select_bulk_email() - @ddt.data(["myself"], ["staff"], ["learners"], ["myself", "staff", "learners"]) - def test_email_queued_for_sending(self, recipient): - self.send_email_page.send_message(recipient) - self.send_email_page.verify_message_queued_successfully() - @attr('a11y') def test_bulk_email_a11y(self): """ @@ -138,99 +120,8 @@ class AutoEnrollmentWithCSVTest(BaseInstructorDashboardTest): instructor_dashboard_page = self.visit_instructor_dashboard() self.auto_enroll_section = instructor_dashboard_page.select_membership().select_auto_enroll_section() # Initialize the page objects - self.register_page = CombinedLoginAndRegisterPage(self.browser, start_page="register") self.dashboard_page = DashboardPage(self.browser) - def test_browse_and_upload_buttons_are_visible(self): - """ - Scenario: On the Membership tab of the Instructor Dashboard, Auto-Enroll Browse and Upload buttons are visible. - Given that I am on the Membership tab on the Instructor Dashboard - Then I see the 'REGISTER/ENROLL STUDENTS' section on the page with the 'Browse' and 'Upload' buttons - """ - self.assertTrue(self.auto_enroll_section.is_file_attachment_browse_button_visible()) - self.assertTrue(self.auto_enroll_section.is_upload_button_visible()) - - def test_enroll_unregister_student(self): - """ - Scenario: On the Membership tab of the Instructor Dashboard, Batch Enrollment div is visible. - Given that I am on the Membership tab on the Instructor Dashboard - Then I enter the email and enroll it. - Logout the current page. - And Navigate to the registration page and register the student. - Then I see the course which enrolled the student. - """ - username = "test_{uuid}".format(uuid=self.unique_id[0:6]) - email = "{user}@example.com".format(user=username) - self.auto_enroll_section.fill_enrollment_batch_text_box(email) - self.assertIn( - 'Successfully sent enrollment emails to the following users. ' - 'They will be enrolled once they register:', - self.auto_enroll_section.get_notification_text() - ) - LogoutPage(self.browser).visit() - self.register_page.visit() - self.register_page.register( - email=email, - password="123456", - username=username, - full_name="Test User", - country="US", - favorite_movie="Harry Potter", - ) - course_names = self.dashboard_page.wait_for_page().available_courses - self.assertEqual(len(course_names), 1) - self.assertIn(self.course_info["display_name"], course_names) - - def test_clicking_file_upload_button_without_file_shows_error(self): - """ - Scenario: Clicking on the upload button without specifying a CSV file results in error. - Given that I am on the Membership tab on the Instructor Dashboard - When I click the Upload Button without specifying a CSV file - Then I should be shown an Error Notification - And The Notification message should read 'File is not attached.' - """ - self.auto_enroll_section.click_upload_file_button() - self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_ERROR)) - self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "File is not attached.") - - def test_uploading_correct_csv_file_results_in_success(self): - """ - Scenario: Uploading a CSV with correct data results in Success. - Given that I am on the Membership tab on the Instructor Dashboard - When I select a csv file with correct data and click the Upload Button - Then I should be shown a Success Notification. - """ - self.auto_enroll_section.upload_correct_csv_file() - self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_SUCCESS)) - - def test_uploading_csv_file_with_bad_data_results_in_errors_and_warnings(self): - """ - Scenario: Uploading a CSV with incorrect data results in error and warnings. - Given that I am on the Membership tab on the Instructor Dashboard - When I select a csv file with incorrect data and click the Upload Button - Then I should be shown an Error Notification - And a corresponding Error Message. - And I should be shown a Warning Notification - And a corresponding Warning Message. - """ - self.auto_enroll_section.upload_csv_file_with_errors_warnings() - self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_ERROR)) - self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "Data in row #2 must have exactly four columns: email, username, full name, and country") - self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_WARNING)) - self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_WARNING), "ename (d@a.com): (An account with email d@a.com exists but the provided username ename is different. Enrolling anyway with d@a.com.)") - - def test_uploading_non_csv_file_results_in_error(self): - """ - Scenario: Uploading an image file for auto-enrollment results in error. - Given that I am on the Membership tab on the Instructor Dashboard - When I select an image file (a non-csv file) and click the Upload Button - Then I should be shown an Error Notification - And The Notification message should read 'Make sure that the file you upload is in CSV..' - """ - self.auto_enroll_section.upload_non_csv_file() - self.assertTrue(self.auto_enroll_section.is_notification_displayed(section_type=self.auto_enroll_section.NOTIFICATION_ERROR)) - self.assertEqual(self.auto_enroll_section.first_notification_message(section_type=self.auto_enroll_section.NOTIFICATION_ERROR), "Make sure that the file you upload is in CSV format with no extraneous characters or rows.") - @attr('a11y') def test_auto_enroll_csv_a11y(self): """ @@ -242,356 +133,6 @@ class AutoEnrollmentWithCSVTest(BaseInstructorDashboardTest): self.auto_enroll_section.a11y_audit.check_for_accessibility_errors() -@attr(shard=10) -class ProctoredExamsTest(BaseInstructorDashboardTest): - """ - End-to-end tests for Proctoring Sections of the Instructor Dashboard. - """ - - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - - def setUp(self): - super(ProctoredExamsTest, self).setUp() - - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - course_fixture = CourseFixture(**self.course_info) - course_fixture.add_advanced_settings({ - "enable_proctored_exams": {"value": "true"} - }) - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section 1').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children( - XBlockFixtureDesc('problem', 'Test Problem 1') - ) - ) - ).install() - - self.dashboard_page = DashboardPage(self.browser) - self.problem_page = ProblemPage(self.browser) - - # Add a verified mode to the course - ModeCreationPage( - self.browser, self.course_id, mode_slug=u'verified', mode_display_name=u'Verified Certificate', - min_price=10, suggested_prices='10,20' - ).visit() - - # Auto-auth register for the course. - self._auto_auth(self.USERNAME, self.EMAIL, False) - - def _auto_auth(self, username, email, staff): - """ - Logout and login with given credentials. - """ - AutoAuthPage(self.browser, username=username, email=email, - course_id=self.course_id, staff=staff).visit() - - def _login_as_a_verified_user(self): - """ - login as a verififed user - """ - - self._auto_auth(self.USERNAME, self.EMAIL, False) - enroll_user_track(self.browser, self.course_id, 'verified') - - def _create_a_proctored_exam_and_attempt(self): - """ - Creates a proctored exam and makes the student attempt it so that - the associated allowance and attempts are visible on the Instructor Dashboard. - """ - # Visit the course outline page in studio - LogoutPage(self.browser).visit() - self._auto_auth("STAFF_TESTER", "staff101@example.com", True) - self.studio_course_outline.visit() - - # open the exam settings to make it a proctored exam. - self.studio_course_outline.open_subsection_settings_dialog() - - # select advanced settings tab - self.studio_course_outline.select_advanced_tab() - - self.studio_course_outline.make_exam_proctored() - - # login as a verified student and visit the courseware. - LogoutPage(self.browser).visit() - self._login_as_a_verified_user() - self.courseware_page.visit() - - # Start the proctored exam. - self.courseware_page.start_proctored_exam() - - def _create_a_timed_exam_and_attempt(self): - """ - Creates a timed exam and makes the student attempt it so that - the associated allowance and attempts are visible on the Instructor Dashboard. - """ - # Visit the course outline page in studio - LogoutPage(self.browser).visit() - self._auto_auth("STAFF_TESTER", "staff101@example.com", True) - self.studio_course_outline.visit() - - # open the exam settings to make it a proctored exam. - self.studio_course_outline.open_subsection_settings_dialog() - - # select advanced settings tab - self.studio_course_outline.select_advanced_tab() - - self.studio_course_outline.make_exam_timed() - - # login as a verified student and visit the courseware. - LogoutPage(self.browser).visit() - self._login_as_a_verified_user() - self.courseware_page.visit() - - # Start the timed exam. - self.courseware_page.start_timed_exam() - - # Stop the timed exam. - self.courseware_page.stop_timed_exam() - LogoutPage(self.browser).visit() - - def test_can_reset_attempts(self): - """ - Make sure that Exam attempts are visible and can be reset. - """ - # Given that an exam has been configured to be a proctored exam. - self._create_a_timed_exam_and_attempt() - - # When I log in as an instructor, - __, __, __, __ = self.log_in_as_instructor() - - # And visit the Student Proctored Exam Attempts Section of Instructor Dashboard's Special Exams tab - instructor_dashboard_page = self.visit_instructor_dashboard() - exam_attempts_section = instructor_dashboard_page.select_special_exams().select_exam_attempts_section() - - # Then I can see the search text field - self.assertTrue(exam_attempts_section.is_search_text_field_visible) - - # And I can see one attempt by a student. - self.assertTrue(exam_attempts_section.is_student_attempt_visible) - - # And I can remove the attempt by clicking the "x" at the end of the row. - exam_attempts_section.remove_student_attempt() - self.assertFalse(exam_attempts_section.is_student_attempt_visible) - - -@attr(shard=10) -class DataDownloadsTest(BaseInstructorDashboardTest): - """ - Bok Choy tests for the "Data Downloads" tab. - """ - def setUp(self): - super(DataDownloadsTest, self).setUp() - self.course_fixture = CourseFixture(**self.course_info).install() - self.instructor_username, self.instructor_id, __, __ = self.log_in_as_instructor( - course_access_roles=['data_researcher'] - ) - instructor_dashboard_page = self.visit_instructor_dashboard() - self.data_download_section = instructor_dashboard_page.select_data_download() - - def verify_report_requested_event(self, report_type): - """ - Verifies that the correct event is emitted when a report is requested. - """ - self.assert_matching_events_were_emitted( - event_filter={'name': u'edx.instructor.report.requested', 'report_type': report_type} - ) - - def verify_report_downloaded_event(self, report_url): - """ - Verifies that the correct event is emitted when a report is downloaded. - """ - self.assert_matching_events_were_emitted( - event_filter={'name': u'edx.instructor.report.downloaded', 'report_url': report_url} - ) - - def verify_report_download(self, report_name): - """ - Verifies that a report can be downloaded and an event fired. - """ - download_links = self.data_download_section.report_download_links - self.assertEqual(len(download_links), 1) - download_links[0].click() - expected_url = download_links.attrs('href')[0] - self.assertIn(report_name, expected_url) - self.verify_report_downloaded_event(expected_url) - - def test_student_profiles_report_download(self): - """ - Scenario: Verify that an instructor can download a student profiles report - - Given that I am an instructor - And I visit the instructor dashboard's "Data Downloads" tab - And I click on the "Download profile information as a CSV" button - Then a report should be generated - And a report requested event should be emitted - When I click on the report - Then a report downloaded event should be emitted - """ - report_name = u"student_profile_info" - self.data_download_section.generate_student_report_button.click() - self.data_download_section.wait_for_available_report() - self.verify_report_requested_event(report_name) - self.verify_report_download(report_name) - - def test_grade_report_download(self): - """ - Scenario: Verify that an instructor can download a grade report - - Given that I am an instructor - And I visit the instructor dashboard's "Data Downloads" tab - And I click on the "Generate Grade Report" button - Then a report should be generated - And a report requested event should be emitted - When I click on the report - Then a report downloaded event should be emitted - """ - report_name = u"grade_report" - self.data_download_section.generate_grade_report_button.click() - self.data_download_section.wait_for_available_report() - self.verify_report_requested_event(report_name) - self.verify_report_download(report_name) - - def test_problem_grade_report_download(self): - """ - Scenario: Verify that an instructor can download a problem grade report - - Given that I am an instructor - And I visit the instructor dashboard's "Data Downloads" tab - And I click on the "Generate Problem Grade Report" button - Then a report should be generated - And a report requested event should be emitted - When I click on the report - Then a report downloaded event should be emitted - """ - report_name = u"problem_grade_report" - self.data_download_section.generate_problem_report_button.click() - self.data_download_section.wait_for_available_report() - self.verify_report_requested_event(report_name) - self.verify_report_download(report_name) - - def test_ora2_response_report_download(self): - """ - Scenario: Verify that an instructor can download an ORA2 grade report - - Given that I am an instructor - And I visit the instructor dashboard's "Data Downloads" tab - And I click on the "Download ORA2 Responses" button - Then a report should be generated - """ - report_name = u"ORA_data" - self.data_download_section.generate_ora2_response_report_button.click() - self.data_download_section.wait_for_available_report() - self.verify_report_download(report_name) - - -@ddt.ddt -class DataDownloadsWithMultipleRoleTests(BaseInstructorDashboardTest): - """ - Bok Choy tests for the "Data Downloads" tab with multiple user roles. - """ - shard = 23 - - def setUp(self): - super(DataDownloadsWithMultipleRoleTests, self).setUp() - self.course_fixture = CourseFixture(**self.course_info).install() - - @ddt.data(['staff'], ['instructor']) - def test_list_student_profile_information_for_large_course(self, role): - """ - Scenario: List enrolled students' profile information for a large course - Given I am "" for a very large course - When I visit the "Data Download" tab - Then I do not see a button to 'List enrolled students' profile information' - Examples: - | Role | - | instructor | - | staff | - - """ - username, __, email, password = self.log_in_as_instructor( - global_staff=False, - course_access_roles=role - ) - instructor_dashboard_page = self.visit_instructor_dashboard() - data_download_section = instructor_dashboard_page.select_data_download() - - self.assertTrue(data_download_section.enrolled_student_profile_button_present) - LogoutPage(self.browser).visit() - for __ in range(5): - learner_username = "test_student_{uuid}".format(uuid=self.unique_id[0:8]) - learner_email = "{user}@example.com".format(user=learner_username) - - # Enroll test users in the course - AutoAuthPage( - self.browser, - username=learner_username, - email=learner_email, - course_id=self.course_id - ).visit() - - # Login again with staff or instructor - AutoAuthPage( - self.browser, - username=username, - email=email, - password=password, - course_id=self.course_id, - staff=False, - course_access_roles=role - ).visit() - - instructor_dashboard_page = self.visit_instructor_dashboard() - instructor_dashboard_page.select_data_download() - self.assertFalse(data_download_section.enrolled_student_profile_button_present) - - @ddt.data(['staff'], ['instructor']) - def test_view_grading_configuration(self, role): - """ - Scenario: View the grading configuration - Given I am "" for a course - When I click "Grading Configuration" - Then I see the grading configuration for the course - Examples: - | Role | - | instructor | - | staff | - """ - expected = u"""----------------------------------------------------------------------------- -Course grader: - - -Graded sections: - subgrader=, type=Homework, category=Homework, weight=0.15 - subgrader=, type=Lab, category=Lab, weight=0.15 - subgrader=, type=Midterm Exam, category=Midterm Exam, weight=0.3 - subgrader=, type=Final Exam, category=Final Exam, weight=0.4 ------------------------------------------------------------------------------ -Listing grading context for course {} -graded sections: -[] -all graded blocks: -length=0""".format(self.course_id) - self.log_in_as_instructor( - global_staff=False, - course_access_roles=role - ) - instructor_dashboard_page = self.visit_instructor_dashboard() - data_download_section = instructor_dashboard_page.select_data_download() - - data_download_section.generate_grading_configuration_button.click() - self.assertEqual(data_download_section.grading_config_text, expected) - - @attr(shard=10) @ddt.ddt class CertificatesTest(BaseInstructorDashboardTest): @@ -618,295 +159,6 @@ class CertificatesTest(BaseInstructorDashboardTest): self.certificates_section = self.instructor_dashboard_page.select_certificates() disable_animations(self.certificates_section) - def test_generate_certificates_buttons_is_disable(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Generate Certificates button is disable. - Given that I am on the Certificates tab on the Instructor Dashboard - The instructor-generation and cert_html_view_enabled feature flags have been enabled - But the certificate is not active in settings. - Then I see a 'Generate Certificates' button disabled - """ - self.test_certificate_config['is_active'] = False - self.cert_fixture.update_certificate(1) - self.browser.refresh() - self.assertFalse(self.certificates_section.generate_certificates_button.visible) - self.assertTrue(self.certificates_section.generate_certificates_disabled_button.visible) - - def test_generate_certificates_buttons_is_visible(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Generate Certificates button is visible. - Given that I am on the Certificates tab on the Instructor Dashboard - And the instructor-generation feature flag has been enabled - Then I see a 'Generate Certificates' button - And when I click on the 'Generate Certificates' button - Then I should see a status message and 'Generate Certificates' button should be disabled. - """ - self.assertTrue(self.certificates_section.generate_certificates_button.visible) - self.certificates_section.generate_certificates_button.click() - alert = get_modal_alert(self.certificates_section.browser) - alert.accept() - - self.certificates_section.wait_for_ajax() - EmptyPromise( - lambda: self.certificates_section.certificate_generation_status.visible, - 'Certificate generation status shown' - ).fulfill() - disabled = self.certificates_section.generate_certificates_button.attrs('disabled') - self.assertEqual(disabled[0], 'true') - - def test_pending_tasks_section_is_visible(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Pending Instructor Tasks section is visible. - Given that I am on the Certificates tab on the Instructor Dashboard - Then I see 'Pending Instructor Tasks' section - """ - self.assertTrue(self.certificates_section.pending_tasks_section.visible) - - def test_certificate_exceptions_section_is_visible(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Certificate Exceptions section is visible. - Given that I am on the Certificates tab on the Instructor Dashboard - Then I see 'CERTIFICATE EXCEPTIONS' section - """ - self.assertTrue(self.certificates_section.certificate_exceptions_section.visible) - - def test_instructor_can_add_certificate_exception(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor can add new certificate - exception to list. - - Given that I am on the Certificates tab on the Instructor Dashboard - When I fill in student username and notes fields and click 'Add Exception' button - Then new certificate exception should be visible in certificate exceptions list - """ - notes = 'Test Notes' - # Add a student to Certificate exception list - self.certificates_section.add_certificate_exception(self.user_name, notes) - self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertIn(notes, self.certificates_section.last_certificate_exception.text) - - # Verify that added exceptions are also synced with backend - # Revisit Page - self.certificates_section.refresh() - - # wait for the certificate exception section to render - self.certificates_section.wait_for_certificate_exceptions_section() - - # validate certificate exception synced with server is visible in certificate exceptions list - self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertIn(notes, self.certificates_section.last_certificate_exception.text) - - def test_remove_certificate_exception_on_page_reload(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor can remove added certificate - exceptions from the list. - - Given that I am on the Certificates tab on the Instructor Dashboard - When I fill in student username and notes fields and click 'Add Exception' button - Then new certificate exception should be visible in certificate exceptions list - - Revisit the page to make sure exceptions are synced. - - Remove the user from the exception list should remove the user from the list. - """ - notes = 'Test Notes' - # Add a student to Certificate exception list - self.certificates_section.add_certificate_exception(self.user_name, notes) - self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertIn(notes, self.certificates_section.last_certificate_exception.text) - - # Verify that added exceptions are also synced with backend - # Revisit Page - self.certificates_section.refresh() - - # Remove Certificate Exception - self.certificates_section.remove_first_certificate_exception() - self.assertNotIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertNotIn(notes, self.certificates_section.last_certificate_exception.text) - - def test_instructor_can_remove_certificate_exception(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor can remove added certificate - exceptions from the list. - - Given that I am on the Certificates tab on the Instructor Dashboard - When I fill in student username and notes fields and click 'Add Exception' button - Then new certificate exception should be visible in certificate exceptions list - """ - notes = 'Test Notes' - # Add a student to Certificate exception list - self.certificates_section.add_certificate_exception(self.user_name, notes) - self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertIn(notes, self.certificates_section.last_certificate_exception.text) - - # Remove Certificate Exception - self.certificates_section.remove_first_certificate_exception() - self.assertNotIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertNotIn(notes, self.certificates_section.last_certificate_exception.text) - - # Verify that added exceptions are also synced with backend - # Revisit Page - self.certificates_section.refresh() - - # wait for the certificate exception section to render - self.certificates_section.wait_for_certificate_exceptions_section() - - # validate certificate exception synced with server is visible in certificate exceptions list - self.assertNotIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertNotIn(notes, self.certificates_section.last_certificate_exception.text) - - def test_error_on_duplicate_certificate_exception(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, - Error message appears if student being added already exists in certificate exceptions list - - Given that I am on the Certificates tab on the Instructor Dashboard - When I fill in student username that already is in the list and click 'Add Exception' button - Then Error Message should say 'User (username/email={user}) already in exception list.' - """ - # Add a student to Certificate exception list - self.certificates_section.add_certificate_exception(self.user_name, '') - - # Add duplicate student to Certificate exception list - self.certificates_section.add_certificate_exception(self.user_name, '') - - self.assertIn( - u'{user} already in exception list.'.format(user=self.user_name), - self.certificates_section.message.text - ) - - def test_error_on_empty_user_name(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, - Error message appears if no username/email is entered while clicking "Add Exception" button - - Given that I am on the Certificates tab on the Instructor Dashboard - When I click on 'Add Exception' button - AND student username/email field is empty - Then Error Message should say - 'Student username/email field is required and can not be empty. ' - 'Kindly fill in username/email and then press "Add Exception" button.' - """ - # Click 'Add Exception' button without filling username/email field - self.certificates_section.wait_for_certificate_exceptions_section() - self.certificates_section.click_add_exception_button() - - self.assertIn( - 'Student username/email field is required and can not be empty. ' - 'Kindly fill in username/email and then press "Add to Exception List" button.', - self.certificates_section.message.text - ) - - def test_error_on_non_existing_user(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, - Error message appears if username/email does not exists in the system while clicking "Add Exception" button - - Given that I am on the Certificates tab on the Instructor Dashboard - When I click on 'Add Exception' button - AND student username/email does not exists - Then Error Message should say - 'Student username/email field is required and can not be empty. ' - 'Kindly fill in username/email and then press "Add Exception" button. - """ - invalid_user = 'test_user_non_existent' - # Click 'Add Exception' button with invalid username/email field - self.certificates_section.wait_for_certificate_exceptions_section() - - self.certificates_section.fill_user_name_field(invalid_user) - self.certificates_section.click_add_exception_button() - self.certificates_section.wait_for_ajax() - - self.assertIn( - u"{user} does not exist in the LMS. Please check your spelling and retry.".format(user=invalid_user), - self.certificates_section.message.text - ) - - def test_user_not_enrolled_error(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, - Error message appears if user is not enrolled in the course while trying to add a new exception. - - Given that I am on the Certificates tab on the Instructor Dashboard - When I click on 'Add Exception' button - AND student is not enrolled in the course - Then Error Message should say - "The user (username/email={user}) you have entered is not enrolled in this course. - Make sure the username or email address is correct, then try again." - """ - new_user = 'test_user_{uuid}'.format(uuid=self.unique_id[6:12]) - new_email = 'test_user_{uuid}@example.com'.format(uuid=self.unique_id[6:12]) - # Create a new user who is not enrolled in the course - AutoAuthPage(self.browser, username=new_user, email=new_email).visit() - # Login as instructor and visit Certificate Section of Instructor Dashboard - self.user_name, self.user_id, __, __ = self.log_in_as_instructor() - self.instructor_dashboard_page.visit() - self.certificates_section = self.instructor_dashboard_page.select_certificates() - - # Click 'Add Exception' button with invalid username/email field - self.certificates_section.wait_for_certificate_exceptions_section() - - self.certificates_section.fill_user_name_field(new_user) - self.certificates_section.click_add_exception_button() - self.certificates_section.wait_for_ajax() - - self.assertIn( - u"{user} is not enrolled in this course. Please check your spelling and retry.".format(user=new_user), - self.certificates_section.message.text - ) - - def test_generate_certificate_exception(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, when user clicks - 'Generate Exception Certificates' newly added certificate exceptions should be synced on server - - Given that I am on the Certificates tab on the Instructor Dashboard - When I click 'Generate Exception Certificates' - Then newly added certificate exceptions should be synced on server - """ - # Add a student to Certificate exception list - self.certificates_section.add_certificate_exception(self.user_name, '') - - # Click 'Generate Exception Certificates' button - self.certificates_section.click_generate_certificate_exceptions_button() - self.certificates_section.wait_for_ajax() - - self.assertIn( - self.user_name + ' has been successfully added to the exception list. Click Generate Exception Certificate' - ' below to send the certificate.', - self.certificates_section.message.text - ) - - @ddt.data( - ('Test \nNotes', 'Test Notes'), - ('Notes', 'Notes'), - ) - @ddt.unpack - def test_notes_escaped_in_add_certificate_exception(self, notes, expected_notes): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor can add new certificate - exception to list. - - Given that I am on the Certificates tab on the Instructor Dashboard - When I fill in student username and notes (which contains character which are needed to be escaped) - and click 'Add Exception' button, then new certificate exception should be visible in - certificate exceptions list. - """ - # Add a student to Certificate exception list - self.certificates_section.add_certificate_exception(self.user_name, notes) - self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertIn(expected_notes, self.certificates_section.last_certificate_exception.text) - - # Revisit Page & verify that added exceptions are also synced with backend - self.certificates_section.refresh() - - # Wait for the certificate exception section to render - self.certificates_section.wait_for_certificate_exceptions_section() - - # Validate certificate exception synced with server is visible in certificate exceptions list - self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text) - self.assertIn(expected_notes, self.certificates_section.last_certificate_exception.text) - @attr('a11y') def test_certificates_a11y(self): """ @@ -977,150 +229,6 @@ class CertificateInvalidationTest(BaseInstructorDashboardTest): disable_animations(self.certificates_section) - def test_instructor_can_invalidate_certificate(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor can add a certificate - invalidation to invalidation list. - - Given that I am on the Certificates tab on the Instructor Dashboard - When I fill in student username and notes fields and click 'Add Exception' button - Then new certificate exception should be visible in certificate exceptions list - """ - notes = 'Test Notes' - # Add a student to certificate invalidation list - self.certificates_section.add_certificate_invalidation(self.student_name, notes) - self.assertIn(self.student_name, self.certificates_section.last_certificate_invalidation.text) - self.assertIn(notes, self.certificates_section.last_certificate_invalidation.text) - - # Validate success message - self.assertIn( - u"Certificate has been successfully invalidated for {user}.".format(user=self.student_name), - self.certificates_section.certificate_invalidation_message.text - ) - - # Verify that added invalidations are also synced with backend - # Revisit Page - self.certificates_section.refresh() - - # wait for the certificate invalidations section to render - self.certificates_section.wait_for_certificate_invalidations_section() - - # validate certificate invalidation is visible in certificate invalidation list - self.assertIn(self.student_name, self.certificates_section.last_certificate_invalidation.text) - self.assertIn(notes, self.certificates_section.last_certificate_invalidation.text) - - def test_instructor_can_re_validate_certificate(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor can re-validate certificate. - - Given that I am on the certificates tab on the Instructor Dashboard - AND there is a certificate invalidation in certificate invalidation table - When I click "Remove from Invalidation Table" button - Then certificate is re-validated and removed from certificate invalidation table. - """ - notes = 'Test Notes' - # Add a student to certificate invalidation list - self.certificates_section.add_certificate_invalidation(self.student_name, notes) - self.assertIn(self.student_name, self.certificates_section.last_certificate_invalidation.text) - self.assertIn(notes, self.certificates_section.last_certificate_invalidation.text) - - # Verify that added invalidations are also synced with backend - # Revisit Page - self.certificates_section.refresh() - - # wait for the certificate invalidations section to render - self.certificates_section.wait_for_certificate_invalidations_section() - - # click "Remove from Invalidation Table" button next to certificate invalidation - self.certificates_section.remove_first_certificate_invalidation() - - # validate certificate invalidation is removed from the list - self.assertNotIn(self.student_name, self.certificates_section.last_certificate_invalidation.text) - self.assertNotIn(notes, self.certificates_section.last_certificate_invalidation.text) - - self.assertIn( - "The certificate for this learner has been re-validated and the system is " - "re-running the grade for this learner.", - self.certificates_section.certificate_invalidation_message.text - ) - - def test_error_on_empty_user_name_or_email(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor should see error message if he clicks - "Invalidate Certificate" button without entering student username or email. - - Given that I am on the certificates tab on the Instructor Dashboard - When I click "Invalidate Certificate" button without entering student username/email. - Then I see following error message - "Student username/email field is required and can not be empty." - "Kindly fill in username/email and then press "Invalidate Certificate" button." - """ - # Click "Invalidate Certificate" with empty student username/email field - self.certificates_section.fill_certificate_invalidation_user_name_field("") - self.certificates_section.click_invalidate_certificate_button() - self.certificates_section.wait_for_ajax() - - self.assertIn( - u'Student username/email field is required and can not be empty. ' - u'Kindly fill in username/email and then press "Invalidate Certificate" button.', - self.certificates_section.certificate_invalidation_message.text - ) - - def test_error_on_invalid_user(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor should see error message if - the student entered for certificate invalidation does not exist. - - Given that I am on the certificates tab on the Instructor Dashboard - When I click "Invalidate Certificate" - AND the username entered does not exist in the system - Then I see following error message - "Student username/email field is required and can not be empty." - "Kindly fill in username/email and then press "Invalidate Certificate" button." - """ - invalid_user = "invalid_test_user" - # Click "Invalidate Certificate" with invalid student username/email - self.certificates_section.fill_certificate_invalidation_user_name_field(invalid_user) - self.certificates_section.click_invalidate_certificate_button() - self.certificates_section.wait_for_ajax() - - self.assertIn( - u"{user} does not exist in the LMS. Please check your spelling and retry.".format(user=invalid_user), - self.certificates_section.certificate_invalidation_message.text - ) - - def test_user_not_enrolled_error(self): - """ - Scenario: On the Certificates tab of the Instructor Dashboard, Instructor should see error message if - the student entered for certificate invalidation is not enrolled in the course. - - Given that I am on the certificates tab on the Instructor Dashboard - When I click "Invalidate Certificate" - AND the username entered is not enrolled in the current course - Then I see following error message - "{user} is not enrolled in this course. Please check your spelling and retry." - """ - new_user = 'test_user_{uuid}'.format(uuid=self.unique_id[6:12]) - new_email = 'test_user_{uuid}@example.com'.format(uuid=self.unique_id[6:12]) - # Create a new user who is not enrolled in the course - AutoAuthPage(self.browser, username=new_user, email=new_email).visit() - # Login as instructor and visit Certificate Section of Instructor Dashboard - self.user_name, self.user_id, __, __ = self.log_in_as_instructor() - self.instructor_dashboard_page.visit() - self.certificates_section = self.instructor_dashboard_page.select_certificates() - - # Click 'Invalidate Certificate' button with not enrolled student - self.certificates_section.wait_for_certificate_invalidations_section() - - self.certificates_section.fill_certificate_invalidation_user_name_field(new_user) - self.certificates_section.click_invalidate_certificate_button() - self.certificates_section.wait_for_ajax() - - self.assertIn( - u"{user} is not enrolled in this course. Please check your spelling and retry.".format(user=new_user), - self.certificates_section.certificate_invalidation_message.text - ) - @attr('a11y') def test_invalidate_certificates_a11y(self): """ @@ -1135,154 +243,3 @@ class CertificateInvalidationTest(BaseInstructorDashboardTest): '.certificates-wrapper' ]) self.certificates_section.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=20) -class EcommerceTest(BaseInstructorDashboardTest): - """ - Bok Choy tests for the "E-Commerce" tab. - """ - def setup_course(self, course_number): - """ - Sets up the course - """ - self.course_info['number'] = course_number - course_fixture = CourseFixture( - self.course_info["org"], - self.course_info["number"], - self.course_info["run"], - self.course_info["display_name"] - ) - course_fixture.install() - - def visit_ecommerce_section(self): - """ - Log in to visit Instructor dashboard and click E-commerce tab - """ - self.log_in_as_instructor(course_access_roles=['finance_admin']) - instructor_dashboard_page = self.visit_instructor_dashboard() - return instructor_dashboard_page.select_ecommerce_tab() - - def add_course_mode(self, sku_value=None): - """ - Add an honor mode to the course - """ - ModeCreationPage(browser=self.browser, course_id=self.course_id, mode_slug=u'honor', min_price=10, - sku=sku_value).visit() - - def test_enrollment_codes_section_visible_for_non_ecommerce_course(self): - """ - Test Enrollment Codes UI, under E-commerce Tab, should be visible in the Instructor Dashboard with non - e-commerce course - """ - # Setup course - non_ecommerce_course_number = "34039497242734583224814321005482849780" - self.setup_course(non_ecommerce_course_number) - - # Add an honor mode to the course - self.add_course_mode() - - # Log in and visit E-commerce section under Instructor dashboard - self.assertIn(u'Enrollment Codes', self.visit_ecommerce_section().get_sections_header_values()) - - def test_coupon_codes_section_visible_for_non_ecommerce_course(self): - """ - Test Coupon Codes UI, under E-commerce Tab, should be visible in the Instructor Dashboard with non - e-commerce course - """ - # Setup course - non_ecommerce_course_number = "34039497242734583224814321005482849781" - self.setup_course(non_ecommerce_course_number) - - # Add an honor mode to the course - self.add_course_mode() - - # Log in and visit E-commerce section under Instructor dashboard - self.assertIn(u'Coupon Code List', self.visit_ecommerce_section().get_sections_header_values()) - - def test_enrollment_codes_section_not_visible_for_ecommerce_course(self): - """ - Test Enrollment Codes UI, under E-commerce Tab, should not be visible in the Instructor Dashboard with - e-commerce course - """ - # Setup course - ecommerce_course_number = "34039497242734583224814321005482849782" - self.setup_course(ecommerce_course_number) - - # Add an honor mode to the course with sku value - self.add_course_mode('test_sku') - - # Log in and visit E-commerce section under Instructor dashboard - self.assertNotIn(u'Enrollment Codes', self.visit_ecommerce_section().get_sections_header_values()) - - def test_coupon_codes_section_not_visible_for_ecommerce_course(self): - """ - Test Coupon Codes UI, under E-commerce Tab, should not be visible in the Instructor Dashboard with - e-commerce course - """ - # Setup course - ecommerce_course_number = "34039497242734583224814321005482849783" - self.setup_course(ecommerce_course_number) - - # Add an honor mode to the course with sku value - self.add_course_mode('test_sku') - - # Log in and visit E-commerce section under Instructor dashboard - self.assertNotIn(u'Coupon Code List', self.visit_ecommerce_section().get_sections_header_values()) - - -class StudentAdminTest(BaseInstructorDashboardTest): - SECTION_NAME = 'Test Section 1' - SUBSECTION_NAME = 'Test Subsection 1' - UNIT_NAME = 'Test Unit 1' - PROBLEM_NAME = 'Test Problem 1' - - shard = 23 - - def setUp(self): - super(StudentAdminTest, self).setUp() - self.course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - self.problem = create_multiple_choice_problem(self.PROBLEM_NAME) - self.vertical = XBlockFixtureDesc('vertical', "Lab Unit") - self.course_fix.add_children( - XBlockFixtureDesc('chapter', self.SECTION_NAME).add_children( - XBlockFixtureDesc('sequential', self.SUBSECTION_NAME).add_children( - self.vertical.add_children(self.problem) - ) - ), - ).install() - - self.username, __, __, __ = self.log_in_as_instructor() - self.instructor_dashboard_page = self.visit_instructor_dashboard() - - def test_rescore_rescorable(self): - student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentSpecificAdmin) - student_admin_section.set_student_email_or_username(self.username) - student_admin_section.set_problem_location(self.problem.locator) - getattr(student_admin_section, 'rescore_button').click() - alert = get_modal_alert(student_admin_section.browser) - alert.dismiss() - self.assertFalse(self.instructor_dashboard_page.is_rescore_unsupported_message_visible()) - - def test_task_list_visibility(self): - """ - Test that instructor task list is visible on student admin section - to users who have access to instructor tab/dashboard - """ - # first check for global staff users - student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentAdminPage) - self.assertTrue(student_admin_section.running_tasks_section.visible) - - # logout global-staff user and check for users with staff access to course - LogoutPage(self.browser).visit() - # having staff access to course is compulsory to access instructor dashboard - self.log_in_as_instructor(False, ['staff']) - self.instructor_dashboard_page = self.visit_instructor_dashboard() - student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentAdminPage) - self.assertTrue(student_admin_section.running_tasks_section.visible) diff --git a/common/test/acceptance/tests/lms/test_lms_lti.py b/common/test/acceptance/tests/lms/test_lms_lti.py deleted file mode 100644 index 622650504e..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_lti.py +++ /dev/null @@ -1,419 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Bok choy acceptance tests for LTI xblock -""" - - -import os - -from common.test.acceptance.pages.lms.instructor_dashboard import ( - GradeBookPage, - InstructorDashboardPage, - StudentAdminPage -) -from common.test.acceptance.pages.lms.progress import ProgressPage -from common.test.acceptance.pages.lms.tab_nav import TabNavPage - -from ...fixtures.course import CourseFixture, XBlockFixtureDesc -from ...pages.lms.courseware import CoursewarePage, LTIContentIframe -from ..helpers import UniqueCourseTest, auto_auth, select_option_by_text - - -class TestLTIConsumer(UniqueCourseTest): - """ - Base class for tests of LTI xblock in the LMS. - """ - - USERNAME = "STUDENT_TESTER" - EMAIL = "student101@example.com" - host = os.environ.get('BOK_CHOY_HOSTNAME', '127.0.0.1') - - def setUp(self): - super(TestLTIConsumer, self).setUp() - self.courseware_page = CoursewarePage(self.browser, self.course_id) - self.lti_iframe = LTIContentIframe(self.browser, self.course_id) - self.tab_nav = TabNavPage(self.browser) - self.progress_page = ProgressPage(self.browser, self.course_id) - self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - self.grade_book_page = GradeBookPage(self.browser) - # Install a course - display_name = "Test Course" + self.unique_id - self.course_fix = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], display_name=display_name - ) - - def test_lti_no_launch_url_is_not_rendered(self): - """ - Scenario: LTI component in LMS with no launch_url is not rendered - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with no_launch_url fields: - Then I view the LTI and error is shown - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'launch_url': '', - 'open_in_a_new_page': False - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertTrue(self.courseware_page.is_error_message_present()) - self.assertFalse(self.courseware_page.is_iframe_present()) - self.assertFalse(self.courseware_page.is_launch_url_present()) - - def test_incorrect_lti_id_is_rendered_incorrectly(self): - """ - Scenario: LTI component in LMS with incorrect lti_id is rendered incorrectly - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with incorrect_lti_id fields: - Then I view the LTI but incorrect_signature warning is rendered - """ - metadata_advance_settings = "test_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'incorrect_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertTrue(self.courseware_page.is_iframe_present()) - self.assertFalse(self.courseware_page.is_launch_url_present()) - self.assertFalse(self.courseware_page.is_error_message_present()) - self.courseware_page.go_to_lti_container() - self.assertEqual("Wrong LTI signature", self.lti_iframe.lti_content) - - def test_incorrect_lti_credentials_is_rendered_incorrectly(self): - """ - Scenario: LTI component in LMS with icorrect LTI credentials is rendered incorrectly - Given the course has incorrect LTI credentials with registered Instructor - the course has an LTI component with correct fields: - I view the LTI but incorrect_signature warning is rendered - """ - metadata_advance_settings = "test_lti_id:test_client_key:incorrect_lti_secret_key" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertTrue(self.courseware_page.is_iframe_present()) - self.assertFalse(self.courseware_page.is_launch_url_present()) - self.assertFalse(self.courseware_page.is_error_message_present()) - self.courseware_page.go_to_lti_container() - self.assertEqual("Wrong LTI signature", self.lti_iframe.lti_content) - - def test_lti_is_rendered_in_iframe_correctly(self): - """ - Scenario: LTI component in LMS is correctly rendered in iframe - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - I view the LTI and it is rendered in iframe correctly - """ - - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False - } - - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertTrue(self.courseware_page.is_iframe_present()) - self.assertFalse(self.courseware_page.is_launch_url_present()) - self.assertFalse(self.courseware_page.is_error_message_present()) - self.courseware_page.go_to_lti_container() - self.assertEqual("This is LTI tool. Success.", self.lti_iframe.lti_content) - - def test_lti_graded_component_for_staff(self): - """ - Scenario: Graded LTI component in LMS is correctly works for staff - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - verify scores on progress and grade book pages. - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False, - 'weight': 10, - 'graded': True, - 'has_score': True - } - expected_scores = [(5, 10)] - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.courseware_page.go_to_lti_container() - self.lti_iframe.submit_lti_answer('#submit-button') - self.assertIn("LTI consumer (edX) responded with XML content", self.lti_iframe.lti_content) - self.lti_iframe.switch_to_default() - self.tab_nav.go_to_tab('Progress') - actual_scores = self.progress_page.scores("Test Chapter", "Test Section") - self.assertEqual(actual_scores, expected_scores) - self.assertEqual(['Overall Score', 'Overall Score\n1%'], self.progress_page.graph_overall_score()) - self.tab_nav.go_to_tab('Instructor') - student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentAdminPage) - student_admin_section.click_grade_book_link() - self.assertEqual("50", self.grade_book_page.get_value_in_the_grade_book('Homework 1 - Test Section', 1)) - self.assertEqual("1", self.grade_book_page.get_value_in_the_grade_book('Total', 1)) - - def test_lti_switch_role_works_correctly(self): - """ - Scenario: Graded LTI component in LMS role's masquerading correctly works - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - switch role from instructor to learner and verify that it works correctly - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False, - 'has_score': True - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertTrue(self.courseware_page.is_iframe_present()) - self.assertFalse(self.courseware_page.is_launch_url_present()) - self.assertFalse(self.courseware_page.is_error_message_present()) - self.courseware_page.go_to_lti_container() - self.assertEqual("This is LTI tool. Success.", self.lti_iframe.lti_content) - self.assertEqual("Role: Instructor", self.lti_iframe.get_user_role) - self.lti_iframe.switch_to_default() - select_option_by_text(self.courseware_page.get_role_selector, 'Learner') - self.courseware_page.wait_for_ajax() - self.assertTrue(self.courseware_page.is_iframe_present()) - self.assertFalse(self.courseware_page.is_launch_url_present()) - self.assertFalse(self.courseware_page.is_error_message_present()) - self.courseware_page.go_to_lti_container() - self.assertEqual("This is LTI tool. Success.", self.lti_iframe.lti_content) - self.assertEqual("Role: Student", self.lti_iframe.get_user_role) - - def test_lti_graded_component_for_learner(self): - """ - Scenario: Graded LTI component in LMS is correctly works for learners - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - verify scores on progress - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False, - 'weight': 10, - 'graded': True, - 'has_score': True - } - expected_scores = [(5, 10)] - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) - self.courseware_page.visit() - self.courseware_page.go_to_lti_container() - self.lti_iframe.submit_lti_answer('#submit-button') - self.assertIn("LTI consumer (edX) responded with XML content", self.lti_iframe.lti_content) - self.lti_iframe.switch_to_default() - self.tab_nav.go_to_tab('Progress') - actual_scores = self.progress_page.scores("Test Chapter", "Test Section") - self.assertEqual(actual_scores, expected_scores) - self.assertEqual(['Overall Score', 'Overall Score\n1%'], self.progress_page.graph_overall_score()) - - def test_lti_v2_callback_graded_component(self): - """ - Scenario: Graded LTI component in LMS is correctly works with LTI2v0 PUT callback - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - verify scores on progress and grade book pages. - verify feedback in LTI component. - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False, - 'weight': 10, - 'graded': True, - 'has_score': True - } - expected_scores = [(8, 10)] - problem_score = '(8.0 / 10.0 points)' - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.courseware_page.go_to_lti_container() - self.lti_iframe.submit_lti_answer("#submit-lti2-button") - self.assertIn("LTI consumer (edX) responded with HTTP 200", self.lti_iframe.lti_content) - self.lti_iframe.switch_to_default() - self.tab_nav.go_to_tab('Progress') - actual_scores = self.progress_page.scores("Test Chapter", "Test Section") - self.assertEqual(actual_scores, expected_scores) - self.assertEqual(['Overall Score', 'Overall Score\n1%'], self.progress_page.graph_overall_score()) - self.tab_nav.go_to_tab('Instructor') - student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentAdminPage) - student_admin_section.click_grade_book_link() - self.assertEqual("80", self.grade_book_page.get_value_in_the_grade_book('Homework 1 - Test Section', 1)) - self.assertEqual("1", self.grade_book_page.get_value_in_the_grade_book('Total', 1)) - self.tab_nav.go_to_tab('Course') - self.assertEqual(problem_score, self.courseware_page.get_elem_text('.problem-progress')) - self.assertEqual("This is awesome.", self.courseware_page.get_elem_text('.problem-feedback')) - - def test_lti_delete_callback_graded_component(self): - """ - Scenario: Graded LTI component in LMS is correctly works with LTI2v0 PUT delete callback - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - Verify LTI provider deletes my grade on progress and grade book page - verify LTI provider deletes feedback from LTI Component - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False, - 'weight': 10, - 'graded': True, - 'has_score': True - } - expected_scores = [(0, 10)] - problem_score = '(8.0 / 10.0 points)' - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.courseware_page.go_to_lti_container() - self.lti_iframe.submit_lti_answer("#submit-lti2-button") - self.assertIn("LTI consumer (edX) responded with HTTP 200", self.lti_iframe.lti_content) - self.lti_iframe.switch_to_default() - self.courseware_page.visit() - self.assertEqual(problem_score, self.courseware_page.get_elem_text('.problem-progress')) - self.assertEqual("This is awesome.", self.courseware_page.get_elem_text('.problem-feedback')) - self.courseware_page.go_to_lti_container() - self.lti_iframe.submit_lti_answer("#submit-lti-delete-button") - self.courseware_page.visit() - self.assertEqual("(10.0 points possible)", self.courseware_page.get_elem_text('.problem-progress')) - self.assertFalse(self.courseware_page.is_lti_component_present('.problem-feedback')) - self.tab_nav.go_to_tab('Progress') - actual_scores = self.progress_page.scores("Test Chapter", "Test Section") - self.assertEqual(actual_scores, expected_scores) - self.assertEqual(['Overall Score', 'Overall Score\n0%'], self.progress_page.graph_overall_score()) - self.tab_nav.go_to_tab('Instructor') - student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentAdminPage) - student_admin_section.click_grade_book_link() - self.assertEqual("0", self.grade_book_page.get_value_in_the_grade_book('Homework 1 - Test Section', 1)) - self.assertEqual("0", self.grade_book_page.get_value_in_the_grade_book('Total', 1)) - - def test_lti_hide_launch_shows_no_button(self): - """ - Scenario: LTI component that set to hide_launch and open_in_a_new_page shows no button - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - verify LTI component don't show launch button with text "LTI (External resource)" - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': False, - 'hide_launch': True - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertFalse(self.courseware_page.is_lti_component_present('.link_lti_new_window')) - self.assertEqual("LTI (External resource)", self.courseware_page.get_elem_text('.problem-header')) - - def test_lti_hide_launch_shows_no_iframe(self): - """ - Scenario: LTI component that set to hide_launch and not open_in_a_new_page shows no iframe - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - verify LTI component don't show LTI iframe with text "LTI (External resource)" - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'open_in_a_new_page': True, - 'hide_launch': True - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertFalse(self.courseware_page.is_lti_component_present('.ltiLaunchFrame')) - self.assertEqual("LTI (External resource)", self.courseware_page.get_elem_text('.problem-header')) - - def test_lti_button_text_correctly_displayed(self): - """ - Scenario: LTI component button text is correctly displayed - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - verify LTI component button with text "Launch Application" - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'button_text': 'Launch Application' - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertEqual("Launch Application", self.courseware_page.get_elem_text('.link_lti_new_window')) - - def test_lti_component_description_correctly_displayed(self): - """ - Scenario: LTI component description is correctly displayed - Given the course has correct LTI credentials with registered Instructor - the course has an LTI component with correct fields: - LTI component description with text "Application description" - """ - metadata_advance_settings = "correct_lti_id:test_client_key:test_client_secret" - metadata_lti_xblock = { - 'lti_id': 'correct_lti_id', - 'launch_url': 'http://{}:{}/{}'.format(self.host, '8765', 'correct_lti_endpoint'), - 'description': 'Application description' - } - self.set_advance_settings(metadata_advance_settings) - self.create_lti_xblock(metadata_lti_xblock) - auto_auth(self.browser, self.USERNAME, self.EMAIL, True, self.course_id) - self.courseware_page.visit() - self.assertEqual("Application description", self.courseware_page.get_elem_text('.lti-description')) - - def set_advance_settings(self, metadata_advance_settings): - - # Set value against advanced modules in advanced settings - self.course_fix.add_advanced_settings({ - "advanced_modules": {"value": ["lti_consumer"]}, - 'lti_passports': {"value": [metadata_advance_settings]} - }) - - def create_lti_xblock(self, metadata_lti_xblock): - self.course_fix.add_children( - XBlockFixtureDesc(category='chapter', display_name='Test Chapter').add_children( - XBlockFixtureDesc( - category='sequential', display_name='Test Section', grader_type='Homework', graded=True - ).add_children( - XBlockFixtureDesc(category='lti', display_name='LTI', metadata=metadata_lti_xblock).add_children( - ) - ) - ) - ).install() diff --git a/common/test/acceptance/tests/lms/test_lms_problems.py b/common/test/acceptance/tests/lms/test_lms_problems.py index b08ca69019..6e49713deb 100644 --- a/common/test/acceptance/tests/lms/test_lms_problems.py +++ b/common/test/acceptance/tests/lms/test_lms_problems.py @@ -4,18 +4,13 @@ Bok choy acceptance tests for problems in the LMS """ -import time -from datetime import datetime, timedelta from textwrap import dedent -import ddt - from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage -from common.test.acceptance.pages.lms.problem import ProblemPage, DragAndDropPage -from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest +from common.test.acceptance.pages.lms.problem import ProblemPage +from common.test.acceptance.tests.helpers import UniqueCourseTest from openedx.core.lib.tests import attr @@ -68,674 +63,6 @@ class ProblemsTest(UniqueCourseTest): return XBlockFixtureDesc('sequential', 'Test Subsection') -@attr(shard=9) -class ProblemClarificationTest(ProblemsTest): - """ - Tests the element that can be used in problem XML. - """ - - def get_problem(self): - """ - Create a problem with a - """ - xml = dedent(u""" - - -

- Given the data in Table 7 Table 7: "Example PV Installation Costs", - Page 171 of Roberts textbook, compute the ROI - Return on Investment (per year) over 20 years. -

- - - - -
-
- """) - return XBlockFixtureDesc('problem', 'TOOLTIP TEST PROBLEM', data=xml) - - def test_clarification(self): - """ - Test that we can see the tooltips. - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_name, 'TOOLTIP TEST PROBLEM') - problem_page.click_clarification(0) - self.assertIn('"Example PV Installation Costs"', problem_page.visible_tooltip_text) - problem_page.click_clarification(1) - tooltip_text = problem_page.visible_tooltip_text - self.assertIn('Return on Investment', tooltip_text) - self.assertIn('per year', tooltip_text) - self.assertNotIn('strong', tooltip_text) - - -@attr(shard=9) -class ProblemHintTest(ProblemsTest, EventsTestMixin): - """ - Base test class for problem hint tests. - """ - def verify_check_hint(self, answer, answer_text, expected_events): - """ - Verify clicking Check shows the extended hint in the problem message. - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_text[0], u'question text') - problem_page.fill_answer(answer) - problem_page.click_submit() - self.assertEqual(problem_page.message_text, answer_text) - # Check for corresponding tracking event - actual_events = self.wait_for_events( - event_filter={'event_type': 'edx.problem.hint.feedback_displayed'}, - number_of_matches=1 - ) - self.assert_events_match(expected_events, actual_events) - - def verify_demand_hints(self, first_hint, second_hint, expected_events): - """ - Test clicking through the demand hints and verify the events sent. - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - - # The hint notification should not be visible on load - self.assertFalse(problem_page.is_hint_notification_visible()) - - # The two Hint button should be enabled. One visible, one present, but not visible in the DOM - self.assertEqual([None, None], problem_page.get_hint_button_disabled_attr()) - - # The hint button rotates through multiple hints - problem_page.click_hint(hint_index=0) - self.assertTrue(problem_page.is_hint_notification_visible()) - self.assertEqual(problem_page.hint_text, first_hint) - # Now there are two "hint" buttons, as there is also one in the hint notification. - self.assertEqual([None, None], problem_page.get_hint_button_disabled_attr()) - - problem_page.click_hint(hint_index=1) - self.assertEqual(problem_page.hint_text, second_hint) - # Now both "hint" buttons should be disabled, as there are no more hints. - self.assertEqual(['true', 'true'], problem_page.get_hint_button_disabled_attr()) - - # Now click on "Review" and make sure the focus goes to the correct place. - problem_page.click_review_in_notification(notification_type='hint') - problem_page.wait_for_focus_on_problem_meta() - - # Check corresponding tracking events - actual_events = self.wait_for_events( - event_filter={'event_type': 'edx.problem.hint.demandhint_displayed'}, - number_of_matches=2 - ) - self.assert_events_match(expected_events, actual_events) - - def get_problem(self): - """ Subclasses should override this to complete the fixture """ - raise NotImplementedError() - - -@attr(shard=9) -class ProblemNotificationTests(ProblemsTest): - """ - Tests that the notifications are visible when expected. - """ - - def get_problem(self): - """ - Problem structure. - """ - xml = dedent(""" - - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - Germany - Indonesia - Russia - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml, - metadata={'max_attempts': 10}, - grader_type='Final Exam') - - def test_notification_updates(self): - """ - Verifies that the notification is removed and not visible when it should be - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - problem_page.click_choice("choice_2") - self.assertFalse(problem_page.is_success_notification_visible()) - problem_page.click_submit() - problem_page.wait_success_notification() - self.assertEqual('Question 1: correct', problem_page.status_sr_text) - - # Clicking Save should clear the submit notification - problem_page.click_save() - self.assertFalse(problem_page.is_success_notification_visible()) - problem_page.wait_for_save_notification() - - # Changing the answer should clear the save notification - problem_page.click_choice("choice_1") - self.assertFalse(problem_page.is_save_notification_visible()) - problem_page.click_save() - problem_page.wait_for_save_notification() - - # Submitting the problem again should clear the save notification - problem_page.click_submit() - problem_page.wait_incorrect_notification() - self.assertEqual('Question 1: incorrect', problem_page.status_sr_text) - self.assertFalse(problem_page.is_save_notification_visible()) - - -@attr(shard=9) -class ProblemFeedbackNotificationTests(ProblemsTest): - """ - Tests that the feedback notifications are visible when expected. - """ - - def get_problem(self): - """ - Problem structure. - """ - xml = dedent(""" - - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - Germany - Indonesia - Russia - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml, - metadata={'max_attempts': 10}, - grader_type='Final Exam') - - def test_feedback_notification_hides_after_save(self): - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - problem_page.click_choice("choice_0") - problem_page.click_submit() - problem_page.wait_for_feedback_message_visibility() - problem_page.click_choice("choice_1") - problem_page.click_save() - self.assertFalse(problem_page.is_feedback_message_notification_visible()) - - -@attr(shard=9) -class ProblemSaveStatusUpdateTests(ProblemsTest): - """ - Tests the problem status updates correctly with an answer change and save. - """ - def get_problem(self): - """ - Problem structure. - """ - xml = dedent(""" - - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - Germany - Indonesia - Russia - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml, - metadata={'max_attempts': 10}, - grader_type='Final Exam') - - def test_status_removed_after_save_before_submit(self): - """ - Scenario: User should see the status removed when saving after submitting an answer and reloading the page. - Given that I have loaded the problem page - And a choice has been selected and submitted - When I change the choice - And Save the problem - And reload the problem page - Then I should see the save notification and I should not see any indication of problem status - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - problem_page.click_choice("choice_1") - problem_page.click_submit() - problem_page.wait_incorrect_notification() - problem_page.wait_for_expected_status('label.choicegroup_incorrect', 'incorrect') - problem_page.click_choice("choice_2") - self.assertFalse(problem_page.is_expected_status_visible('label.choicegroup_incorrect')) - problem_page.click_save() - problem_page.wait_for_save_notification() - # Refresh the page and the status should not be added - self.courseware_page.visit() - self.assertFalse(problem_page.is_expected_status_visible('label.choicegroup_incorrect')) - self.assertTrue(problem_page.is_save_notification_visible()) - - -@attr(shard=9) -class ProblemSubmitButtonMaxAttemptsTest(ProblemsTest): - """ - Tests that the Submit button disables after the number of max attempts is reached. - """ - - def get_problem(self): - """ - Problem structure. - """ - xml = dedent(""" - - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - Germany - Indonesia - Russia - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml, - metadata={'max_attempts': 2}, - grader_type='Final Exam') - - def test_max_attempts(self): - """ - Verifies that the Submit button disables when the max number of attempts is reached. - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - - # Submit first answer (correct) - problem_page.click_choice("choice_2") - self.assertFalse(problem_page.is_submit_disabled()) - problem_page.click_submit() - problem_page.wait_success_notification() - - # Submit second and final answer (incorrect) - problem_page.click_choice("choice_1") - problem_page.click_submit() - problem_page.wait_incorrect_notification() - - # Make sure that the Submit button disables. - problem_page.wait_for_submit_disabled() - - -@attr(shard=9) -class ProblemSubmitButtonPastDueTest(ProblemsTest): - """ - Tests that the Submit button is disabled if it is past the due date. - """ - - def get_problem(self): - """ - Problem structure. - """ - xml = dedent(""" - - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - Germany - Indonesia - Russia - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml, - metadata={'max_attempts': 2}, - grader_type='Final Exam') - - def get_sequential(self): - """ Subclasses can override this to add a sequential with metadata """ - return XBlockFixtureDesc('sequential', 'Test Subsection', metadata={'due': "2016-10-01T00"}) - - def test_past_due(self): - """ - Verifies that the Submit button disables when the max number of attempts is reached. - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - # Should have Submit button disabled on original rendering. - problem_page.wait_for_submit_disabled() - - # Select a choice, and make sure that the Submit button remains disabled. - problem_page.click_choice("choice_2") - problem_page.wait_for_submit_disabled() - - -@attr(shard=19) -class ProblemExtendedHintTest(ProblemHintTest, EventsTestMixin): - """ - Test that extended hint features plumb through to the page html and tracking log. - """ - - def get_problem(self): - """ - Problem with extended hint features. - """ - xml = dedent(""" - -

question text

- - hint - - - - demand-hint1 - demand-hint2 - -
- """) - return XBlockFixtureDesc('problem', 'TITLE', data=xml) - - def test_check_hint(self): - """ - Test clicking Check shows the extended hint in the problem message. - """ - self.verify_check_hint( - 'B', - u'Answer\nIncorrect: hint', - [ - { - 'event': - { - 'hint_label': u'Incorrect:', - 'trigger_type': 'single', - 'student_answer': [u'B'], - 'correctness': False, - 'question_type': 'stringresponse', - 'hints': [{'text': 'hint'}] - } - } - ] - ) - - def test_demand_hint(self): - """ - Test clicking hint button shows the demand hint in its div. - """ - self.verify_demand_hints( - u'Hint (1 of 2): demand-hint1', - u'Hint (1 of 2): demand-hint1\nHint (2 of 2): demand-hint2', - [ - {'event': {u'hint_index': 0, u'hint_len': 2, u'hint_text': u'demand-hint1'}}, - {'event': {u'hint_index': 1, u'hint_len': 2, u'hint_text': u'demand-hint2'}} - ] - ) - - -@attr(shard=9) -class ProblemHintWithHtmlTest(ProblemHintTest, EventsTestMixin): - """ - Tests that hints containing html get rendered properly - """ - - def get_problem(self): - """ - Problem with extended hint features. - """ - xml = dedent(""" - -

question text

- - aa bb cc - - - - aa bb cc - dd ee ff - -
- """) - return XBlockFixtureDesc('problem', 'PROBLEM HTML HINT TEST', data=xml) - - def test_check_hint(self): - """ - Test clicking Check shows the extended hint in the problem message. - """ - self.verify_check_hint( - 'C', - u'Answer\nIncorrect: aa bb cc', - [ - { - 'event': - { - 'hint_label': u'Incorrect:', - 'trigger_type': 'single', - 'student_answer': [u'C'], - 'correctness': False, - 'question_type': 'stringresponse', - 'hints': [{'text': 'aa bb cc'}] - } - } - ] - ) - - def test_demand_hint(self): - """ - Test clicking hint button shows the demand hints in a notification area. - """ - self.verify_demand_hints( - u'Hint (1 of 2): aa bb cc', - u'Hint (1 of 2): aa bb cc\nHint (2 of 2): dd ee ff', - [ - {'event': {u'hint_index': 0, u'hint_len': 2, u'hint_text': u'aa bb cc'}}, - {'event': {u'hint_index': 1, u'hint_len': 2, u'hint_text': u'dd ee ff'}} - ] - ) - - -@attr(shard=9) -class ProblemWithMathjax(ProblemsTest): - """ - Tests the used in problem - """ - - def get_problem(self): - """ - Create a problem with a in body and hint - """ - xml = dedent(r""" - -

Check mathjax has rendered [mathjax]E=mc^2[/mathjax]

- - - - Choice1 Correct choice message - Choice2Wrong choice message - - - - mathjax should work1 \(E=mc^2\) - mathjax should work2 [mathjax]E=mc^2[/mathjax] - -
- """) - return XBlockFixtureDesc('problem', 'MATHJAX TEST PROBLEM', data=xml) - - def test_mathjax_in_hint(self): - """ - Test that MathJax have successfully rendered in problem hint - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_name, "MATHJAX TEST PROBLEM") - - problem_page.verify_mathjax_rendered_in_problem() - - # The hint button rotates through multiple hints - problem_page.click_hint(hint_index=0) - self.assertEqual( - ["Hint (1 of 2): mathjax should work1"], - problem_page.extract_hint_text_from_html - ) - problem_page.verify_mathjax_rendered_in_hint() - - # Rotate the hint and check the problem hint - problem_page.click_hint(hint_index=1) - - self.assertEqual( - [ - "Hint (1 of 2): mathjax should work1", - "Hint (2 of 2): mathjax should work2" - ], - problem_page.extract_hint_text_from_html - ) - - problem_page.verify_mathjax_rendered_in_hint() - - -@attr(shard=9) -class ProblemPartialCredit(ProblemsTest): - """ - Makes sure that the partial credit is appearing properly. - """ - def get_problem(self): - """ - Create a problem with partial credit. - """ - xml = dedent(""" - -

The answer is 1. Partial credit for -1.

- - - - - - -
- """) - return XBlockFixtureDesc('problem', 'PARTIAL CREDIT TEST PROBLEM', data=xml) - - # TODO: Reinstate this, it broke when landing the unified header in LEARNER- - # def test_partial_credit(self): - # """ - # Test that we can see the partial credit value and feedback. - # """ - # self.courseware_page.visit() - # problem_page = ProblemPage(self.browser) - # self.assertEqual(problem_page.problem_name, 'PARTIAL CREDIT TEST PROBLEM') - # problem_page.fill_answer_numerical('-1') - # problem_page.click_submit() - # problem_page.wait_for_status_icon() - # self.assertTrue(problem_page.simpleprob_is_partially_correct()) - - -@attr(shard=9) -class LogoutDuringAnswering(ProblemsTest): - """ - Tests for the scenario where a user is logged out (their session expires - or is revoked) just before they click "check" on a problem. - """ - def get_problem(self): - """ - Create a problem. - """ - xml = dedent(""" - - - - - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml) - - def log_user_out(self): - """ - Log the user out by deleting their session cookie. - """ - self.browser.delete_cookie('sessionid') - - def test_logout_after_click_redirect(self): - """ - 1) User goes to a problem page. - 2) User fills out an answer to the problem. - 3) User is logged out because their session id is invalidated or removed. - 4) User clicks "check", and sees a confirmation modal asking them to - re-authenticate, since they've just been logged out. - 5) User clicks "ok". - 6) User is redirected to the login page. - 7) User logs in. - 8) User is redirected back to the problem page they started out on. - 9) User is able to submit an answer - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_name, 'TEST PROBLEM') - problem_page.fill_answer_numerical('1') - - self.log_user_out() - with problem_page.handle_alert(confirm=True): - problem_page.click_submit() - - login_page = CombinedLoginAndRegisterPage(self.browser) - login_page.wait_for_page() - - login_page.login(self.email, self.password) - - problem_page.wait_for_page() - self.assertEqual(problem_page.problem_name, 'TEST PROBLEM') - - problem_page.fill_answer_numerical('1') - problem_page.click_submit() - self.assertTrue(problem_page.simpleprob_is_correct()) - - -@attr(shard=9) -class ProblemQuestionDescriptionTest(ProblemsTest): - """TestCase Class to verify question and description rendering.""" - descriptions = [ - "A vegetable is an edible part of a plant in tuber form.", - "A fruit is a fertilized ovary of a plant and contains seeds." - ] - - def get_problem(self): - """ - Create a problem with question and description. - """ - xml = dedent(u""" - - - - {} - {} - - vegetable - fruit - - - - """.format(*self.descriptions)) - return XBlockFixtureDesc('problem', 'Label with Description', data=xml) - - def test_question_with_description(self): - """ - Scenario: Test that question and description are rendered as expected. - Given I am enrolled in a course. - When I visit a unit page with a CAPA question. - Then label and description should be rendered correctly. - """ - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_name, 'Label with Description') - self.assertEqual(problem_page.problem_question, 'Eggplant is a _____?') - self.assertEqual(problem_page.problem_question_descriptions, self.descriptions) - - class CAPAProblemA11yBaseTestMixin(object): """Base TestCase Class to verify CAPA problem accessibility.""" @@ -877,325 +204,3 @@ class ProblemMathExpressionInputA11yTest(CAPAProblemA11yBaseTestMixin, ProblemsT """) return XBlockFixtureDesc('problem', 'MATHEXPRESSIONINPUT PROBLEM', data=xml) - - -class ProblemMetaGradedTest(ProblemsTest): - """ - TestCase Class to verify that the graded variable is passed - """ - shard = 23 - - def get_problem(self): - """ - Problem structure - """ - xml = dedent(""" - - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - Germany - Indonesia - Russia - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml, grader_type='Final Exam') - - def test_grader_type_displayed(self): - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_name, 'TEST PROBLEM') - self.assertEqual(problem_page.problem_progress_graded_value, "1 point possible (graded)") - - -class ProblemMetaUngradedTest(ProblemsTest): - """ - TestCase Class to verify that the ungraded variable is passed - """ - shard = 23 - - def get_problem(self): - """ - Problem structure - """ - xml = dedent(""" - - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - Germany - Indonesia - Russia - - - - """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml) - - def test_grader_type_displayed(self): - self.courseware_page.visit() - problem_page = ProblemPage(self.browser) - self.assertEqual(problem_page.problem_name, 'TEST PROBLEM') - self.assertEqual(problem_page.problem_progress_graded_value, "1 point possible (ungraded)") - - -class FormulaProblemTest(ProblemsTest): - """ - Test Class to verify the formula problem on LMS. - """ - shard = 23 - - def setUp(self): - """ - Setup the test suite to verify various behaviors involving formula problem type. - - Given a course, setup a formula problem type and view it in courseware - Given the MathJax requirement for generating preview, wait for MathJax files to load - """ - super(FormulaProblemTest, self).setUp() - self.courseware_page.visit() - time.sleep(6) - - def get_problem(self): - """ - creating the formula response problem, with reset button enabled. - """ - xml = dedent(""" - - -

You can use this template as a guide to the OLX markup to use for math expression problems. Edit this component to replace the example with your own assessment.

- - You can add an optional tip or note related to the prompt like this. Example: To test this example, the correct answer is R_1*R_2/R_3 - - -
-
- """) - return XBlockFixtureDesc('problem', 'TEST PROBLEM', data=xml, metadata={'show_reset_button': True}) - - def test_reset_button_not_rendered_after_correct_submission(self): - """ - Scenario: Verify that formula problem can not be resetted after an incorrect submission. - - Given I am attempting a formula response problem type - When I input a correct answer - Then I should be able to see the mathjax generated preview - When I submit the answer - Then the correct status is visible - And reset button is not rendered - """ - problem_page = ProblemPage(self.browser) - problem_page.fill_answer_numerical('R_1*R_2/R_3') - problem_page.verify_mathjax_rendered_in_preview() - problem_page.click_submit() - self.assertTrue(problem_page.simpleprob_is_correct()) - self.assertFalse(problem_page.is_reset_button_present()) - - def test_reset_problem_after_changing_correctness(self): - """ - Scenario: Verify that formula problem can be resetted after changing the correctness. - - Given I am attempting a formula problem type - When I answer it correctly - Then the correctness status should be visible - And reset button is not rendered - When I change my submission to incorrect - Then the reset button appears and is clickable - """ - problem_page = ProblemPage(self.browser) - problem_page.fill_answer_numerical('R_1*R_2/R_3') - problem_page.verify_mathjax_rendered_in_preview() - problem_page.click_submit() - self.assertTrue(problem_page.simpleprob_is_correct()) - self.assertFalse(problem_page.is_reset_button_present()) - problem_page.fill_answer_numerical('R_1/R_3') - problem_page.click_submit() - self.assertFalse(problem_page.simpleprob_is_correct()) - self.assertTrue(problem_page.is_reset_button_present()) - problem_page.click_reset() - self.assertEqual(problem_page.get_numerical_input_value, '') - - -@ddt.ddt -class FormulaProblemRandomizeTest(ProblemsTest): - """ - Test Class to verify the formula problem on LMS with Randomization enabled. - """ - shard = 23 - - def setUp(self): - """ - Setup the test suite to verify various behaviors involving formula problem type. - - Given a course, setup a formula problem type and view it in courseware - Given the MathJax requirement for generating preview, wait for MathJax files to load - """ - super(FormulaProblemRandomizeTest, self).setUp() - self.courseware_page.visit() - time.sleep(6) - - def get_problem(self): - """ - creating the formula response problem. - """ - xml = dedent(""" - - -

You can use this template as a guide to the OLX markup to use for math expression problems. Edit this component to replace the example with your own assessment.

- - You can add an optional tip or note related to the prompt like this. Example: To test this example, the correct answer is R_1*R_2/R_3 - - -
-
- """) - - # rerandomize:always will show reset button, no matter the submission correctness - return XBlockFixtureDesc( - 'problem', 'TEST PROBLEM', data=xml, metadata={'show_reset_button': True, 'rerandomize': 'always'} - ) - - @ddt.data( - ('R_1*R_2', 'incorrect', 'R_1*R_2/R_3'), - ('R_1*R_2/R_3', 'correct', 'R_1/R_3') - ) - @ddt.unpack - def test_reset_correctness_after_changing_answer(self, input_value, correctness, next_input): - """ - Scenario: Test that formula problem can be resetted after changing the answer. - - Given I am attempting a formula problem type with randomization:always configuration - When I input an answer - Then the mathjax generated preview should be visible - When I submit the problem, I can see the correctness status - When I only input another answer - Then the correctness status is no longer visible - And I am able to see the reset button - And when I click the reset button - Then input pane contents are cleared - """ - problem_page = ProblemPage(self.browser) - problem_page.fill_answer_numerical(input_value) - problem_page.verify_mathjax_rendered_in_preview() - problem_page.click_submit() - self.assertEqual(problem_page.get_simpleprob_correctness(), correctness) - problem_page.fill_answer_numerical(next_input) - self.assertIsNone(problem_page.get_simpleprob_correctness()) - self.assertTrue(problem_page.is_reset_button_present()) - problem_page.click_reset() - self.assertEqual(problem_page.get_numerical_input_value, '') - - -@ddt.ddt -class DragAndDropXblockWithMixinsTest(UniqueCourseTest): - """ - Test Suite to verify various behaviors of DragAndDrop Xblock on the LMS. - """ - - def setUp(self): - super(DragAndDropXblockWithMixinsTest, self).setUp() - - self.username = "test_student_{uuid}".format(uuid=self.unique_id[0:8]) - self.email = "{username}@example.com".format(username=self.username) - self.password = "keep it secret; keep it safe." - self.courseware_page = CoursewarePage(self.browser, self.course_id) - - # Install a course with a hierarchy and problems - self.course_fixture = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'], - start_date=datetime.now() + timedelta(days=10) - ) - self.browser.set_window_size(1024, 1024) - - def setup_sequential(self, metadata): - """ - Setup a sequential with DnD problem, alongwith the metadata provided. - - This method will allow to customize the sequential, such as changing the - due date for individual tests. - """ - problem = self.get_problem() - sequential = self.get_sequential(metadata=metadata) - self.course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - sequential.add_children(problem) - ) - ).install() - - # Auto-auth register for the course. - AutoAuthPage( - self.browser, - username=self.username, - email=self.email, - password=self.password, - course_id=self.course_id, - staff=True - ).visit() - - def format_date(self, date_value): - """ - Get the date in isoformat as this is required format to add date data - in the sequential. - """ - return date_value.isoformat() - - def get_problem(self): - """ - Creating a DnD problem with assessment mode - """ - return XBlockFixtureDesc('drag-and-drop-v2', 'DnD', metadata={'mode': "assessment"}) - - def get_sequential(self, metadata=None): - return XBlockFixtureDesc('sequential', 'Test Subsection', metadata=metadata) - - @ddt.data( - (datetime.now(), True), - (datetime.now() - timedelta(days=1), True), - (datetime.now() + timedelta(days=1), False) - ) - @ddt.unpack - def test_submit_button_status_with_due_date(self, due_date, is_button_disabled): - """ - Scenario: Test that DnD submit button will be enabled if section is not past due. - - Given I have a sequential in instructor-paced course - And a DnD problem with assessment mode is present in the sequential - When I visit the problem - Then the submit button should be present - And button should be disabled as some item needs to be on a zone - When I drag an item to a zone - Then submit button will be enabled if due date has not passed, else disabled - """ - problem_page = DragAndDropPage(self.browser) - self.setup_sequential(metadata={'due': self.format_date(due_date)}) - self.courseware_page.visit() - self.assertTrue(problem_page.is_submit_button_present()) - self.assertTrue(problem_page.is_submit_disabled()) - problem_page.drag_item_to_zone(0, 'middle') - self.assertEqual(is_button_disabled, problem_page.is_submit_disabled()) - - def test_submit_button_when_pacing_change_self_paced(self): - """ - Scenario: For a self-paced course, the submit button of DnD problems will be - be enabled, regardless of the subsection due date. - - Given a DnD problem in a subsection with past due date - And the course is instructor-paced - Then the submit button will remain disabled after initial drag - When the pacing is changed to self-paced - Then the submit button is not disabled anymore - """ - problem_page = DragAndDropPage(self.browser) - self.setup_sequential(metadata={'due': self.format_date(datetime.now())}) - self.courseware_page.visit() - problem_page.drag_item_to_zone(0, 'middle') - self.assertTrue(problem_page.is_submit_disabled()) - self.course_fixture.add_course_details({'self_paced': True}) - self.course_fixture.configure_course() - self.courseware_page.visit() - self.assertFalse(problem_page.is_submit_disabled()) diff --git a/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py b/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py deleted file mode 100644 index 05c629cd44..0000000000 --- a/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Test courseware search -""" - - -import json - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.logout import LogoutPage -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage -from common.test.acceptance.tests.helpers import create_user_partition_json, remove_file -from common.test.acceptance.tests.studio.base_studio_test import ContainerBase -from xmodule.partitions.partitions import Group - - -class SplitTestCoursewareSearchTest(ContainerBase): - """ - Test courseware search on Split Test Module. - """ - shard = 1 - USERNAME = 'STUDENT_TESTER' - EMAIL = 'student101@example.com' - - TEST_INDEX_FILENAME = "test_root/index_file.dat" - - def setUp(self, is_staff=True): - """ - Create search page and course content to search - """ - # create test file in which index for this test will live - with open(self.TEST_INDEX_FILENAME, "w+") as index_file: - json.dump({}, index_file) - self.addCleanup(remove_file, self.TEST_INDEX_FILENAME) - - super(SplitTestCoursewareSearchTest, self).setUp(is_staff=is_staff) - self.staff_user = self.user - - self.course_home_page = CourseHomePage(self.browser, self.course_id) - self.studio_course_outline = StudioCourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self._create_group_configuration() - self._studio_reindex() - - def _auto_auth(self, username, email, staff): - """ - Logout and login with given credentials. - """ - LogoutPage(self.browser).visit() - AutoAuthPage(self.browser, username=username, email=email, course_id=self.course_id, staff=staff).visit() - - def _studio_reindex(self): - """ - Reindex course content on studio course page - """ - self._auto_auth(self.staff_user["username"], self.staff_user["email"], True) - self.studio_course_outline.visit() - self.studio_course_outline.start_reindex() - self.studio_course_outline.wait_for_ajax() - - def _create_group_configuration(self): - """ - Create a group configuration for course - """ - # pylint: disable=protected-access - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - u"user_partitions": [ - create_user_partition_json( - 0, - "Configuration A/B", - "Content Group Partition.", - [Group("0", "Group A"), Group("1", "Group B")] - ) - ] - } - }) - - def populate_course_fixture(self, course_fixture): - """ - Populate the children of the test course fixture. - """ - course_fixture.add_advanced_settings({ - u"advanced_modules": {"value": ["split_test"]}, - }) - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc( - 'html', - 'VISIBLE TO A', - data='VISIBLE TO A', - metadata={"group_access": {0: [0]}} - ), - XBlockFixtureDesc( - 'html', - 'VISIBLE TO B', - data='VISIBLE TO B', - metadata={"group_access": {0: [1]}} - ) - ) - ) - ) - ) - - def test_search_for_experiment_content_user_assigned_to_one_group(self): - """ - Test user can search for experiment content restricted to his group - when assigned to just one experiment group - """ - self._auto_auth(self.USERNAME, self.EMAIL, False) - self.course_home_page.visit() - course_search_results_page = self.course_home_page.search_for_term("VISIBLE TO") - assert "result-excerpt" in course_search_results_page.search_results.html[0] diff --git a/common/test/acceptance/tests/lms/test_lms_user_preview.py b/common/test/acceptance/tests/lms/test_lms_user_preview.py index 94d35cac05..b2107eeb62 100644 --- a/common/test/acceptance/tests/lms/test_lms_user_preview.py +++ b/common/test/acceptance/tests/lms/test_lms_user_preview.py @@ -9,7 +9,6 @@ from textwrap import dedent from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage from common.test.acceptance.tests.helpers import UniqueCourseTest, create_user_partition_json from openedx.core.lib.tests import attr @@ -54,54 +53,6 @@ class StaffViewTest(UniqueCourseTest): return staff_page -@attr(shard=20) -class CourseWithoutContentGroupsTest(StaffViewTest): - """ - Setup for tests that have no content restricted to specific content groups. - """ - - def populate_course_fixture(self, course_fixture): - """ - Populates test course with chapter, sequential, and 2 problems. - """ - problem_data = dedent(""" - -

Choose Yes.

- - - Yes - - -
- """) - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('problem', 'Test Problem 1', data=problem_data), - XBlockFixtureDesc('problem', 'Test Problem 2', data=problem_data) - ) - ) - ) - - -@attr(shard=20) -class StaffViewToggleTest(CourseWithoutContentGroupsTest): - """ - Tests for the staff view toggle button. - """ - def test_instructor_tab_visibility(self): - """ - Test that the instructor tab is hidden when viewing as a student. - """ - - course_page = self._goto_staff_page() - self.assertTrue(course_page.has_tab('Instructor')) - course_page.set_staff_view_mode('Learner') - self.assertEqual(course_page.staff_view_mode, 'Learner') - self.assertFalse(course_page.has_tab('Instructor')) - - @attr(shard=20) class CourseWithContentGroupsTest(StaffViewTest): """ @@ -183,91 +134,6 @@ class CourseWithContentGroupsTest(StaffViewTest): ) ) - def test_staff_sees_all_problems(self): - """ - Scenario: Staff see all problems - Given I have a course with a cohort user partition - And problems that are associated with specific groups in the user partition - When I view the courseware in the LMS with staff access - Then I see all the problems, regardless of their group_access property - """ - course_page = self._goto_staff_page() - verify_expected_problem_visibility( - self, - course_page, - [self.alpha_text, self.beta_text, self.audit_text, self.everyone_text] - ) - - def test_student_not_in_content_group(self): - """ - Scenario: When previewing as a learner, only content visible to all is shown - Given I have a course with a cohort user partition - And problems that are associated with specific groups in the user partition - When I view the courseware in the LMS with staff access - And I change to previewing as a Learner - Then I see only problems visible to all users - """ - course_page = self._goto_staff_page() - course_page.set_staff_view_mode('Learner') - verify_expected_problem_visibility(self, course_page, [self.everyone_text]) - - def test_as_student_in_alpha(self): - """ - Scenario: When previewing as a learner in group alpha, only content visible to alpha is shown - Given I have a course with a cohort user partition - And problems that are associated with specific groups in the user partition - When I view the courseware in the LMS with staff access - And I change to previewing as a Learner in group alpha - Then I see only problems visible to group alpha - """ - course_page = self._goto_staff_page() - course_page.set_staff_view_mode('Learner in alpha') - verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.everyone_text]) - - def test_as_student_in_beta(self): - """ - Scenario: When previewing as a learner in group beta, only content visible to beta is shown - Given I have a course with a cohort user partition - And problems that are associated with specific groups in the user partition - When I view the courseware in the LMS with staff access - And I change to previewing as a Learner in group beta - Then I see only problems visible to group beta - """ - course_page = self._goto_staff_page() - course_page.set_staff_view_mode('Learner in beta') - verify_expected_problem_visibility(self, course_page, [self.beta_text, self.everyone_text]) - - def test_as_student_in_audit(self): - """ - Scenario: When previewing as a learner in the audit enrollment track, only content visible to audit is shown - Given I have a course with an enrollment_track user partition - And problems that are associated with specific groups in the user partition - When I view the courseware in the LMS with staff access - And I change to previewing as a Learner in audit enrollment track - Then I see only problems visible to audit enrollment track - """ - course_page = self._goto_staff_page() - course_page.set_staff_view_mode('Learner in Audit') - verify_expected_problem_visibility(self, course_page, [self.audit_text, self.everyone_text]) - - def create_cohorts_and_assign_students(self, student_a_username, student_b_username): - """ - Adds 2 manual cohorts, linked to content groups, to the course. - Each cohort is assigned one learner. - """ - instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - instructor_dashboard_page.visit() - cohort_management_page = instructor_dashboard_page.select_cohort_management() - cohort_management_page.is_cohorted = True - - def add_cohort_with_student(cohort_name, content_group, student): - """ Create cohort and assign learner to it. """ - cohort_management_page.add_cohort(cohort_name, content_group=content_group) - cohort_management_page.add_students_to_selected_cohort([student]) - add_cohort_with_student("Cohort Alpha", "alpha", student_a_username) - add_cohort_with_student("Cohort Beta", "beta", student_b_username) - cohort_management_page.wait_for_ajax() - @attr('a11y') def test_course_page(self): """ @@ -286,14 +152,3 @@ class CourseWithContentGroupsTest(StaffViewTest): ] }) course_page.a11y_audit.check_for_accessibility_errors() - - -def verify_expected_problem_visibility(test, courseware_page, expected_problems): - """ - Helper method that checks that the expected problems are visible on the current page. - """ - courseware_page.wait_for( - lambda: courseware_page.num_xblock_components == len(expected_problems), "Expected number of problems visible" - ) - for index, expected_problem in enumerate(expected_problems): - test.assertIn(expected_problem, courseware_page.xblock_components[index].text) diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py index 7ca007b373..9958b7b482 100644 --- a/common/test/acceptance/tests/lms/test_problem_types.py +++ b/common/test/acceptance/tests/lms/test_problem_types.py @@ -8,10 +8,8 @@ import textwrap from abc import ABCMeta, abstractmethod import ddt -import pytest import six from bok_choy.promise import BrokenPromise -from six.moves import range from capa.tests.response_xml_factory import ( AnnotationResponseXMLFactory, @@ -179,377 +177,6 @@ class ProblemTypeA11yTestMixin(object): self.problem_page.a11y_audit.check_for_accessibility_errors() -@ddt.ddt -class ProblemTypeTestMixin(ProblemTypeA11yTestMixin): - """ - Test cases shared amongst problem types. - """ - can_submit_blank = False - can_update_save_notification = True - - @attr(shard=11) - def test_answer_correctly(self): - """ - Scenario: I can answer a problem correctly - Given External graders respond "correct" - And I am viewing a "" problem - When I answer a "" problem "correctly" - Then my "" answer is marked "correct" - And The "" problem displays a "correct" answer - And a success notification is shown - And clicking on "Review" moves focus to the problem meta area - And a "problem_check" server event is emitted - And a "problem_check" browser event is emitted - """ - # Make sure we're looking at the right problem - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - - # Answer the problem correctly - self.answer_problem(correctness='correct') - self.problem_page.click_submit() - self.wait_for_status('correct') - self.problem_page.wait_success_notification() - # Check that clicking on "Review" goes to the problem meta location - self.problem_page.click_review_in_notification(notification_type='submit') - self.problem_page.wait_for_focus_on_problem_meta() - - # Check for corresponding tracking event - expected_events = [ - { - 'event_source': 'server', - 'event_type': 'problem_check', - 'username': self.username, - }, { - 'event_source': 'browser', - 'event_type': 'problem_check', - 'username': self.username, - }, - ] - - for event in expected_events: - self.wait_for_events(event_filter=event, number_of_matches=1) - - @attr(shard=11) - def test_answer_incorrectly(self): - """ - Scenario: I can answer a problem incorrectly - Given External graders respond "incorrect" - And I am viewing a "" problem - When I answer a "" problem "incorrectly" - Then my "" answer is marked "incorrect" - And The "" problem displays a "incorrect" answer - """ - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - - # Answer the problem incorrectly - self.answer_problem(correctness='incorrect') - self.problem_page.click_submit() - self.wait_for_status('incorrect') - self.problem_page.wait_incorrect_notification() - - @attr(shard=11) - def test_submit_blank_answer(self): - """ - Scenario: I can submit a blank answer - Given I am viewing a "" problem - When I submit a problem - Then my "" answer is marked "incorrect" - And The "" problem displays a "blank" answer - """ - if not self.can_submit_blank: - pytest.skip("Test incompatible with the current problem type") - - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - # Leave the problem unchanged and assure submit is disabled. - self.wait_for_status('unanswered') - self.assertFalse(self.problem_page.is_submit_disabled()) - self.problem_page.click_submit() - self.wait_for_status('incorrect') - - @attr(shard=11) - def test_cant_submit_blank_answer(self): - """ - Scenario: I can't submit a blank answer - When I try to submit blank answer - Then I can't submit a problem - """ - if self.can_submit_blank: - pytest.skip("Test incompatible with the current problem type") - - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - self.assertTrue(self.problem_page.is_submit_disabled()) - - @attr(shard=12) - def test_can_show_answer(self): - """ - Scenario: Verifies that show answer button is working as expected. - - Given that I am on courseware page - And I can see a CAPA problem with show answer button - When I click "Show Answer" button - And I should see question's solution - And I should see the problem title is focused - """ - self.problem_page.click_show() - self.problem_page.wait_for_show_answer_notification() - - @attr(shard=12) - def test_save_reaction(self): - """ - Scenario: Verify that the save button performs as expected with problem types - - Given that I am on a problem page - And I can see a CAPA problem with the Save button present - When I select and answer and click the "Save" button - Then I should see the Save notification - And the Save button should not be disabled - And clicking on "Review" moves focus to the problem meta area - And if I change the answer selected - Then the Save notification should be removed - """ - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - self.problem_page.wait_for_page() - self.answer_problem(correctness='correct') - self.assertTrue(self.problem_page.is_save_button_enabled()) - self.problem_page.click_save() - # Ensure "Save" button is enabled after save is complete. - self.assertTrue(self.problem_page.is_save_button_enabled()) - self.problem_page.wait_for_save_notification() - # Check that clicking on "Review" goes to the problem meta location - self.problem_page.click_review_in_notification(notification_type='save') - self.problem_page.wait_for_focus_on_problem_meta() - - # Not all problems will detect the change and remove the save notification - if self.can_update_save_notification: - self.answer_problem(correctness='incorrect') - self.assertFalse(self.problem_page.is_save_notification_visible()) - - @attr(shard=12) - def test_reset_shows_errors(self): - """ - Scenario: Reset will show server errors - If I reset a problem without first answering it - Then a "gentle notification" is shown - And the focus moves to the "gentle notification" - """ - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - self.wait_for_status('unanswered') - self.assertFalse(self.problem_page.is_gentle_alert_notification_visible()) - # Click reset without first answering the problem (possible because show_reset_button is set to True) - self.problem_page.click_reset() - self.problem_page.wait_for_gentle_alert_notification() - - @attr(shard=12) - def test_partially_complete_notifications(self): - """ - Scenario: If a partially correct problem is submitted the correct notification is shown - If I submit an answer that is partially correct - Then the partially correct notification should be shown - """ - - # Not all problems have partially correct solutions configured - if not self.partially_correct: - pytest.skip("Test incompatible with the current problem type") - - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - - self.wait_for_status('unanswered') - # Set an answer - self.answer_problem(correctness='partially-correct') - self.problem_page.click_submit() - self.problem_page.wait_partial_notification() - - @ddt.data('correct', 'incorrect') - def test_reset_problem(self, correctness): - """ - Scenario: I can reset a problem - - Given I am viewing a problem with randomization: always and with reset button: on - And I answer a problem as - When I reset the problem - Then my answer is marked "unanswered" - And The problem displays a "blank" answer - """ - self.answer_problem(correctness) - self.problem_page.click_submit() - self.problem_page.click_reset() - self.assertTrue(self.problem_status('unanswered')) - - -@ddt.ddt -class ChangingAnswerOfProblemTestMixin(object): - """ - Test the effect of changing the answers of problem - """ - - @ddt.data(['correct', '1/1 point (ungraded)'], ['incorrect', '0/1 point (ungraded)']) - @ddt.unpack - def test_checkbox_score_after_answer_and_reset(self, correctness, score): - """ - Scenario: I can see my score on problem when I answer it and after I reset it - - Given I am viewing problem - When I answer problem with - Then I should see a - When I reset the problem - Then I should see a score of points possible: 0/1 point (ungraded) - """ - self.answer_problem(correctness) - self.problem_page.click_submit() - self.assertEqual(self.problem_page.problem_progress_graded_value, score) - self.problem_page.click_reset() - self.assertEqual(self.problem_page.problem_progress_graded_value, '0/1 point (ungraded)') - - @ddt.data(['correct', 'incorrect'], ['incorrect', 'correct']) - @ddt.unpack - def test_reset_correctness_after_changing_answer(self, initial_correctness, other_correctness): - """ - Scenario: I can reset the correctness of a problem after changing my answer - - Given I am viewing problem - Then my problem's answer is marked "unanswered" - When I answer and submit the problem with - Then my problem's answer is marked with - And I input an answer as - Then my problem's answer is marked "unanswered" - """ - self.assertTrue(self.problem_status('unanswered')) - self.answer_problem(initial_correctness) - self.problem_page.click_submit() - - self.assertTrue(self.problem_status(initial_correctness)) - - self.answer_problem(other_correctness) - self.assertTrue(self.problem_status('unanswered')) - - -@ddt.ddt -class NonRandomizedProblemTypeTestMixin(ProblemTypeA11yTestMixin): - """ - Test the effect of 'randomization: never' - """ - can_submit_blank = False - can_update_save_notification = True - - def test_non_randomized_problem_correctly(self): - """ - Scenario: The reset button doesn't show up - - Given I am viewing a problem with "randomization": never and with "reset button": on - And I answer problem problem problem correctly - Then The "Reset" button does not appear - """ - self.answer_problem("correct") - self.problem_page.click_submit() - self.assertFalse(self.problem_page.is_reset_button_present()) - - def test_non_randomized_problem_incorrectly(self): - """ - Scenario: I can reset a non-randomized problem that I answered incorrectly - - Given I am viewing problem with "randomization": never and with "reset button": on - And I answer problem incorrectly - When I reset the problem - Then my problem answer is marked "unanswered" - And the problem problem displays a "blank" answer - """ - self.answer_problem("incorrect") - self.problem_page.click_submit() - self.problem_page.click_reset() - self.assertTrue(self.problem_status('unanswered')) - - -@ddt.ddt -class ProblemNeverShowCorrectnessMixin(object): - """ - Tests the effect of adding `show_correctness: never` to the sequence metadata - for subclasses of ProblemTypeTestMixin. - """ - sequential_metadata = {'show_correctness': 'never'} - - @attr(shard=7) - @ddt.data('correct', 'incorrect', 'partially-correct') - def test_answer_says_submitted(self, correctness): - """ - Scenario: I can answer a problem ly - Given External graders respond "" - And I am viewing a "" problem - in a subsection with show_correctness set to "never" - Then I should see a score of "N point(s) possible (ungraded, results hidden)" - When I answer a "" problem "ly" - And the "" problem displays only a "submitted" notification. - And I should see a score of "N point(s) possible (ungraded, results hidden)" - And a "problem_check" server event is emitted - And a "problem_check" browser event is emitted - """ - - # Not all problems have partially correct solutions configured - if correctness == 'partially-correct' and not self.partially_correct: - pytest.skip("Test incompatible with the current problem type") - - # Problem progress text depends on points possible - possible = 'possible (ungraded, results hidden)' - if self.problem_points == 1: - problem_progress = u'1 point {}'.format(possible) - else: - problem_progress = u'{} points {}'.format(self.problem_points, possible) - - # Make sure we're looking at the right problem - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - - # Learner can see that score will be hidden prior to submitting answer - self.assertEqual(self.problem_page.problem_progress_graded_value, problem_progress) - - # Answer the problem correctly - self.answer_problem(correctness=correctness) - self.problem_page.click_submit() - self.wait_for_status('submitted') - self.problem_page.wait_submitted_notification() - - # Score is still hidden after submitting answer - self.assertEqual(self.problem_page.problem_progress_graded_value, problem_progress) - - # Check for corresponding tracking event - expected_events = [ - { - 'event_source': 'server', - 'event_type': 'problem_check', - 'username': self.username, - }, { - 'event_source': 'browser', - 'event_type': 'problem_check', - 'username': self.username, - }, - ] - - for event in expected_events: - self.wait_for_events(event_filter=event, number_of_matches=1) - - class AnnotationProblemTypeBase(ProblemTypeTestBase): """ ProblemTypeTestBase specialization for Annotation Problem Type @@ -615,7 +242,7 @@ class AnnotationProblemTypeBase(ProblemTypeTestBase): ).nth(choice).click() -class AnnotationProblemTypeTest(AnnotationProblemTypeBase, ProblemTypeTestMixin): +class AnnotationProblemTypeTest(AnnotationProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Annotation Problem Type """ @@ -623,13 +250,6 @@ class AnnotationProblemTypeTest(AnnotationProblemTypeBase, ProblemTypeTestMixin) pass -class AnnotationProblemTypeNeverShowCorrectnessTest(AnnotationProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Annotation Problem Type problems. - """ - pass - - class CheckboxProblemTypeBase(ProblemTypeTestBase): """ ProblemTypeTestBase specialization Checkbox Problem Type @@ -664,29 +284,14 @@ class CheckboxProblemTypeBase(ProblemTypeTestBase): @ddt.ddt -class CheckboxProblemTypeTest(CheckboxProblemTypeBase, ProblemTypeTestMixin, ChangingAnswerOfProblemTestMixin): +class CheckboxProblemTypeTest(CheckboxProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Checkbox Problem Type """ shard = 18 - def test_can_show_answer(self): - """ - Scenario: Verifies that show answer button is working as expected. - Given that I am on courseware page - And I can see a CAPA problem with show answer button - When I click "Show Answer" button - And I should see question's solution - And I should see correct choices highlighted - """ - self.problem_page.click_show() - self.assertTrue(self.problem_page.is_solution_tag_present()) - self.assertTrue(self.problem_page.is_correct_choice_highlighted(correct_choices=[1, 3])) - self.problem_page.wait_for_show_answer_notification() - - -class CheckboxProblemTypeTestNonRandomized(CheckboxProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class CheckboxProblemTypeTestNonRandomized(CheckboxProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for the non-randomized checkbox problem """ @@ -704,13 +309,6 @@ class CheckboxProblemTypeTestNonRandomized(CheckboxProblemTypeBase, NonRandomize ) -class CheckboxProblemTypeNeverShowCorrectnessTest(CheckboxProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Checkbox Problem Type problems. - """ - pass - - @ddt.ddt class MultipleChoiceProblemTypeBase(ProblemTypeTestBase): """ @@ -763,92 +361,15 @@ class MultipleChoiceProblemTypeBase(ProblemTypeTestBase): @ddt.ddt -class MultipleChoiceProblemTypeTest(MultipleChoiceProblemTypeBase, ProblemTypeTestMixin): +class MultipleChoiceProblemTypeTest(MultipleChoiceProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Multiple Choice Problem Type """ shard = 24 - def test_can_show_answer(self): - """ - Scenario: Verifies that show answer button is working as expected. - - Given that I am on courseware page - And I can see a CAPA problem with show answer button - When I click "Show Answer" button - The correct answer is displayed with a single correctness indicator. - """ - # Click the correct answer, but don't submit yet. No correctness shows. - self.answer_problem('correct') - self.assertFalse(self.problem_page.is_correct_choice_highlighted(correct_choices=[3])) - - # After submit, the answer should be marked as correct. - self.problem_page.click_submit() - self.assertTrue(self.problem_page.is_correct_choice_highlighted(correct_choices=[3], show_answer=False)) - - # Switch to an incorrect answer. This will hide the correctness indicator. - self.answer_problem('incorrect') - self.assertFalse(self.problem_page.is_correct_choice_highlighted(correct_choices=[3])) - - # Now click Show Answer. A single correctness indicator should be shown. - self.problem_page.click_show() - self.assertTrue(self.problem_page.is_correct_choice_highlighted(correct_choices=[3])) - - # Finally, make sure that clicking Show Answer moved focus to the correct place. - self.problem_page.wait_for_show_answer_notification() - @ddt.ddt -class MultipleChoiceProblemResetCorrectnessAfterChangingAnswerTest(MultipleChoiceProblemTypeBase): - """ - Tests for Multiple choice problem with changing answers - """ - shard = 18 - - @ddt.data(['correct', '1/1 point (ungraded)'], ['incorrect', '0/1 point (ungraded)']) - @ddt.unpack - def test_mcq_score_after_answer_and_reset(self, correctness, score): - """ - Scenario: I can see my score on a multiple choice problem when I answer it and after I reset it - - Given I am viewing a multiple choice problem - When I answer a multiple choice problem - Then I should see a - When I reset the problem - Then I should see a score of points possible: 0/1 point (ungraded) - """ - self.answer_problem(correctness) - self.problem_page.click_submit() - self.assertEqual(self.problem_page.problem_progress_graded_value, score) - self.problem_page.click_reset() - self.assertEqual(self.problem_page.problem_progress_graded_value, '0/1 point (ungraded)') - - @ddt.data(['correct', 'incorrect'], ['incorrect', 'correct']) - @ddt.unpack - def test_reset_correctness_after_changing_answer(self, initial_correctness, other_correctness): - """ - Scenario: I can reset the correctness of a multiple choice problem after changing my answer - - Given I am viewing a multiple choice problem - When I answer a multiple choice problem - Then my multiple choice answer is marked - And I reset the problem - Then my multiple choice answer is NOT marked - And my multiple choice answer is NOT marked - """ - self.assertTrue(self.problem_status("unanswered")) - self.answer_problem(initial_correctness) - self.problem_page.click_submit() - - self.assertTrue(self.problem_status(initial_correctness)) - self.problem_page.click_reset() - - self.assertFalse(self.problem_status(initial_correctness)) - self.assertFalse(self.problem_status(other_correctness)) - - -@ddt.ddt -class MultipleChoiceProblemTypeTestNonRandomized(MultipleChoiceProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class MultipleChoiceProblemTypeTestNonRandomized(MultipleChoiceProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for non-randomized multiple choice problem """ @@ -866,131 +387,6 @@ class MultipleChoiceProblemTypeTestNonRandomized(MultipleChoiceProblemTypeBase, metadata={'rerandomize': 'never', 'show_reset_button': True, 'max_attempts': 3} ) - def test_non_randomized_multiple_choice_with_multiple_attempts(self): - """ - Scenario: I can answer a problem with multiple attempts correctly but cannot reset because randomization is off - - Given I am viewing a randomization "never" "multiple choice" problem with "3" attempts with reset - Then I should see "You have used 0 of 3 attempts" somewhere in the page - When I answer a "multiple choice" problem "correctly" - Then The "Reset" button does not appear - """ - self.assertEqual( - self.problem_page.submission_feedback, - "You have used 0 of 3 attempts", - "All 3 attempts are not available" - ) - - self.answer_problem("correct") - self.problem_page.click_submit() - self.assertFalse(self.problem_page.is_reset_button_present()) - - -class MultipleChoiceProblemTypeTestOneAttempt(MultipleChoiceProblemTypeBase): - """ - Test Multiple choice problem with single attempt - """ - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'never', 'show_reset_button': True, 'max_attempts': 1} - ) - - def test_answer_with_one_attempt_correctly(self): - """ - Scenario: I can answer a problem with one attempt correctly and can not reset - - Given I am viewing a "multiple choice" problem with "1" attempt - When I answer a "multiple choice" problem "correctly" - Then The "Reset" button does not appear - """ - self.answer_problem("correct") - self.problem_page.click_submit() - self.assertFalse(self.problem_page.is_reset_button_present()) - - -class MultipleChoiceProblemTypeTestMultipleAttempt(MultipleChoiceProblemTypeBase): - """ - Test Multiple choice problem with multiple attempts - """ - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'rerandomize': 'always', 'show_reset_button': True, 'max_attempts': 3} - ) - - def test_answer_with_multiple_attempt_correctly(self): - """ - Scenario: I can answer a problem with multiple attempts correctly and still reset the problem - - Given I am viewing a "multiple choice" problem with "3" attempts - Then I should see "You have used 0 of 3 attempts" somewhere in the page - When I answer a "multiple choice" problem "correctly" - Then The "Reset" button does appear - """ - self.assertEqual( - self.problem_page.submission_feedback, - "You have used 0 of 3 attempts", - "All 3 attempts are not available" - ) - self.answer_problem("correct") - self.problem_page.click_submit() - self.assertTrue(self.problem_page.is_reset_button_present()) - - def test_learner_can_see_attempts_left(self): - """ - Scenario: I can view how many attempts I have left on a problem - - Given I am viewing a "multiple choice" problem with "3" attempts - Then I should see "You have used 0 of 3 attempts" somewhere in the page - When I answer a "multiple choice" problem "incorrectly" - And I reset the problem - Then I should see "You have used 1 of 3 attempts" somewhere in the page - When I answer a "multiple choice" problem "incorrectly" - And I reset the problem - Then I should see "You have used 2 of 3 attempts" somewhere in the page - And The "Submit" button does appear - When I answer a "multiple choice" problem "correctly" - Then The "Reset" button does not appear - """ - for attempts_used in range(3): - self.assertEqual( - self.problem_page.submission_feedback, - u"You have used {} of 3 attempts".format(str(attempts_used)), - "All 3 attempts are not available" - ) - if attempts_used == 2: - self.assertTrue(self.problem_page.is_submit_disabled()) - self.answer_problem("correct") - self.problem_page.click_submit() - self.assertFalse(self.problem_page.is_reset_button_present()) - else: - self.answer_problem("incorrect") - self.problem_page.click_submit() - self.problem_page.click_reset() - - -class MultipleChoiceProblemTypeNeverShowCorrectnessTest(MultipleChoiceProblemTypeBase, - ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Multiple Choice Problem Type problems. - """ - pass - class RadioProblemTypeBase(ProblemTypeTestBase): """ @@ -1044,7 +440,7 @@ class RadioProblemTypeBase(ProblemTypeTestBase): @ddt.ddt -class RadioProblemTypeTest(RadioProblemTypeBase, ProblemTypeTestMixin): +class RadioProblemTypeTest(RadioProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Multiple Radio Problem Type """ @@ -1052,56 +448,7 @@ class RadioProblemTypeTest(RadioProblemTypeBase, ProblemTypeTestMixin): pass -@ddt.ddt -class RadioProblemResetCorrectnessAfterChangingAnswerTest(RadioProblemTypeBase): - """ - Tests for Radio problem with changing answers - """ - shard = 24 - - @ddt.data(['correct', '1/1 point (ungraded)'], ['incorrect', '0/1 point (ungraded)']) - @ddt.unpack - def test_radio_score_after_answer_and_reset(self, correctness, score): - """ - Scenario: I can see my score on a radio problem when I answer it and after I reset it - - Given I am viewing a radio problem - When I answer a radio problem - Then I should see a - When I reset the problem - Then I should see a score of points possible: 0/1 point (ungraded) - """ - self.answer_problem(correctness) - self.problem_page.click_submit() - self.assertEqual(self.problem_page.problem_progress_graded_value, score) - self.problem_page.click_reset() - self.assertEqual(self.problem_page.problem_progress_graded_value, '0/1 point (ungraded)') - - @ddt.data(['correct', 'incorrect'], ['incorrect', 'correct']) - @ddt.unpack - def test_reset_correctness_after_changing_answer(self, initial_correctness, other_correctness): - """ - Scenario: I can reset the correctness of a radio problem after changing my answer - - Given I am viewing a radio problem - When I answer a radio problem with - Then my radio answer is marked - And I reset the problem - Then my radio problem's answer is NOT marked - And my radio problem's answer is NOT marked - """ - self.assertTrue(self.problem_status("unanswered")) - self.answer_problem(initial_correctness) - self.problem_page.click_submit() - - self.assertTrue(self.problem_status(initial_correctness)) - self.problem_page.click_reset() - - self.assertFalse(self.problem_status(initial_correctness)) - self.assertFalse(self.problem_status(other_correctness)) - - -class RadioProblemTypeTestNonRandomized(RadioProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class RadioProblemTypeTestNonRandomized(RadioProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for non-randomized radio problem """ @@ -1120,13 +467,6 @@ class RadioProblemTypeTestNonRandomized(RadioProblemTypeBase, NonRandomizedProbl ) -class RadioProblemTypeNeverShowCorrectnessTest(RadioProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Radio Problem Type problems. - """ - pass - - class DropDownProblemTypeBase(ProblemTypeTestBase): """ ProblemTypeTestBase specialization for Drop Down Problem Type @@ -1155,7 +495,7 @@ class DropDownProblemTypeBase(ProblemTypeTestBase): @ddt.ddt -class DropdownProblemTypeTest(DropDownProblemTypeBase, ProblemTypeTestMixin, ChangingAnswerOfProblemTestMixin): +class DropdownProblemTypeTest(DropDownProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Dropdown Problem Type """ @@ -1164,7 +504,7 @@ class DropdownProblemTypeTest(DropDownProblemTypeBase, ProblemTypeTestMixin, Cha @ddt.ddt -class DropDownProblemTypeTestNonRandomized(DropDownProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class DropDownProblemTypeTestNonRandomized(DropDownProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for non-randomized Dropdown problem """ @@ -1183,13 +523,6 @@ class DropDownProblemTypeTestNonRandomized(DropDownProblemTypeBase, NonRandomize ) -class DropDownProblemTypeNeverShowCorrectnessTest(DropDownProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Drop Down Problem Type problems. - """ - pass - - class StringProblemTypeBase(ProblemTypeTestBase): """ ProblemTypeTestBase specialization for String Problem Type @@ -1239,7 +572,7 @@ class StringProblemTypeBase(ProblemTypeTestBase): self.problem_page.fill_answer(textvalue) -class StringProblemTypeTest(StringProblemTypeBase, ProblemTypeTestMixin): +class StringProblemTypeTest(StringProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the String Problem Type """ @@ -1247,13 +580,6 @@ class StringProblemTypeTest(StringProblemTypeBase, ProblemTypeTestMixin): pass -class StringProblemTypeNeverShowCorrectnessTest(StringProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for String Problem Type problems. - """ - pass - - class NumericalProblemTypeBase(ProblemTypeTestBase): """ ProblemTypeTestBase specialization for Numerical Problem Type @@ -1311,39 +637,15 @@ class NumericalProblemTypeBase(ProblemTypeTestBase): @ddt.ddt -class NumericalProblemTypeTest(NumericalProblemTypeBase, ProblemTypeTestMixin, ChangingAnswerOfProblemTestMixin): +class NumericalProblemTypeTest(NumericalProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Numerical Problem Type """ shard = 12 - def test_error_input_gentle_alert(self): - """ - Scenario: I can answer a problem with erroneous input and will see a gentle alert - Given a Numerical Problem type - I can input a string answer - Then I will see a Gentle alert notification - And focus will shift to that notification - And clicking on "Review" moves focus to the problem meta area - """ - # Make sure we're looking at the right problem - self.problem_page.wait_for( - lambda: self.problem_page.problem_name == self.problem_name, - "Make sure the correct problem is on the page" - ) - - # Answer the problem with an erroneous input to cause a gentle alert - self.assertFalse(self.problem_page.is_gentle_alert_notification_visible()) - self.answer_problem(correctness='error') - self.problem_page.click_submit() - self.problem_page.wait_for_gentle_alert_notification() - # Check that clicking on "Review" goes to the problem meta location - self.problem_page.click_review_in_notification(notification_type='gentle-alert') - self.problem_page.wait_for_focus_on_problem_meta() - @ddt.ddt -class NumericalProblemTypeTestNonRandomized(NumericalProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class NumericalProblemTypeTestNonRandomized(NumericalProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for non-randomized Numerical problem """ @@ -1362,42 +664,6 @@ class NumericalProblemTypeTestNonRandomized(NumericalProblemTypeBase, NonRandomi ) -class NumericalProblemTypeTestViewAnswer(NumericalProblemTypeBase): - """ - Test learner can view Numerical problem's answer - """ - - def get_problem(self): - """ - Creates a {problem_type} problem - """ - # Generate the problem XML using capa.tests.response_xml_factory - return XBlockFixtureDesc( - 'problem', - self.problem_name, - data=self.factory.build_xml(**self.factory_kwargs), - metadata={'showanswer': 'always'} - ) - - def test_learner_can_view_answer(self): - """ - Scenario: I can view the answer if the problem has it: - - Given I am viewing a "numerical" that shows the answer "always" - When I press the button with the label "Show Answer" - And I should see "4.14159" somewhere in the page - """ - self.problem_page.click_show() - self.assertEqual(self.problem_page.answer, '4.14159') - - -class NumericalProblemTypeNeverShowCorrectnessTest(NumericalProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Numerical Problem Type problems. - """ - pass - - @ddt.ddt class FormulaProblemTypeBase(ProblemTypeTestBase): """ @@ -1450,13 +716,6 @@ class FormulaProblemTypeBase(ProblemTypeTestBase): self.problem_page.fill_answer(textvalue) -class FormulaProblemTypeNeverShowCorrectnessTest(FormulaProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Formula Problem Type problems. - """ - pass - - @ddt.ddt class ScriptProblemTypeBase(ProblemTypeTestBase): """ @@ -1527,7 +786,7 @@ class ScriptProblemTypeBase(ProblemTypeTestBase): @ddt.ddt -class ScriptProblemTypeTest(ScriptProblemTypeBase, ProblemTypeTestMixin): +class ScriptProblemTypeTest(ScriptProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Script Problem Type """ @@ -1535,36 +794,7 @@ class ScriptProblemTypeTest(ScriptProblemTypeBase, ProblemTypeTestMixin): pass -@ddt.ddt -class ScriptProblemResetAfterAnswerTest(ScriptProblemTypeBase): - """ - Test Script problem by resetting answers - """ - shard = 8 - - @ddt.data(['correct', 'incorrect'], ['incorrect', 'correct']) - @ddt.unpack - def test_reset_correctness_after_changing_answer(self, initial_correctness, other_correctness): - """ - Scenario: I can reset the correctness of a problem after changing my answer - - Given I am viewing a script problem - Then my script problem's answer is marked "unanswered" - When I answer a script problem initial correctness - And I input an answer on a script problem other correctness - Then my script problem answer is marked "unanswered" - """ - self.assertTrue(self.problem_status('unanswered')) - self.answer_problem(initial_correctness) - self.problem_page.click_submit() - - self.assertTrue(self.problem_status(initial_correctness)) - - self.answer_problem(other_correctness) - self.assertTrue(self.problem_status('unanswered')) - - -class ScriptProblemTypeTestNonRandomized(ScriptProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class ScriptProblemTypeTestNonRandomized(ScriptProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for non-randomized Script problem """ @@ -1583,13 +813,6 @@ class ScriptProblemTypeTestNonRandomized(ScriptProblemTypeBase, NonRandomizedPro ) -class ScriptProblemTypeNeverShowCorrectnessTest(ScriptProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Script Problem Type problems. - """ - pass - - class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin): """ TestCase Class for jsinput (custom JavaScript) problem type. @@ -1651,33 +874,12 @@ class CodeProblemTypeBase(ProblemTypeTestBase): pass -class CodeProblemTypeTest(CodeProblemTypeBase, ProblemTypeTestMixin): +class CodeProblemTypeTest(CodeProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Code Problem Type """ shard = 12 - def test_answer_incorrectly(self): - """ - Overridden for script test because the testing grader always responds - with "correct" - """ - pass - - def test_submit_blank_answer(self): - """ - Overridden for script test because the testing grader always responds - with "correct" - """ - pass - - def test_cant_submit_blank_answer(self): - """ - Overridden for script test because the testing grader always responds - with "correct" - """ - pass - def wait_for_status(self, status): """ Overridden for script test because the testing grader always responds @@ -1686,13 +888,6 @@ class CodeProblemTypeTest(CodeProblemTypeBase, ProblemTypeTestMixin): pass -class CodeProblemTypeNeverShowCorrectnessTest(CodeProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Code Problem Type problems. - """ - pass - - class ChoiceTextProblemTypeTestBase(ProblemTypeTestBase): """ Base class for "Choice + Text" Problem Types. @@ -1791,7 +986,7 @@ class RadioTextProblemTypeBase(ChoiceTextProblemTypeTestBase): @ddt.ddt -class RadioTextProblemTypeTest(RadioTextProblemTypeBase, ProblemTypeTestMixin): +class RadioTextProblemTypeTest(RadioTextProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Radio Text Problem Type """ @@ -1799,56 +994,7 @@ class RadioTextProblemTypeTest(RadioTextProblemTypeBase, ProblemTypeTestMixin): pass -@ddt.ddt -class RadioTextProblemResetCorrectnessAfterChangingAnswerTest(RadioTextProblemTypeBase): - """ - Tests for Radio Text problem with changing answers - """ - shard = 18 - - @ddt.data(['correct', '1/1 point (ungraded)'], ['incorrect', '0/1 point (ungraded)']) - @ddt.unpack - def test_mcq_score_after_answer_and_reset(self, correctness, score): - """ - Scenario: I can see my score on a radio text problem when I answer it and after I reset it - - Given I am viewing a radio text problem - When I answer a radio text problem correct/incorrect - Then I should see a score - When I reset the problem - Then I should see a score of points possible: (1/1 point (ungraded) -- 0/1 point (ungraded) - """ - self.answer_problem(correctness) - self.problem_page.click_submit() - self.assertEqual(self.problem_page.problem_progress_graded_value, score) - self.problem_page.click_reset() - self.assertEqual(self.problem_page.problem_progress_graded_value, '0/1 point (ungraded)') - - @ddt.data(['correct', 'incorrect'], ['incorrect', 'correct']) - @ddt.unpack - def test_reset_correctness_after_changing_answer(self, initial_correctness, other_correctness): - """ - Scenario: I can reset the correctness of a multiple choice problem after changing my answer - - Given I am viewing a radio text problem - When I answer a radio text problem InitialCorrectness - Then my radio text answer is marked InitialCorrectness - And I reset the problem - Then my answer is NOT marked InitialCorrectness - And my answer is NOT marked OtherCorrectness - """ - self.assertTrue(self.problem_status("unanswered")) - self.answer_problem(initial_correctness) - self.problem_page.click_submit() - - self.assertTrue(self.problem_status(initial_correctness)) - self.problem_page.click_reset() - - self.assertFalse(self.problem_status(initial_correctness)) - self.assertFalse(self.problem_status(other_correctness)) - - -class RadioTextProblemTypeTestNonRandomized(RadioTextProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class RadioTextProblemTypeTestNonRandomized(RadioTextProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for non-randomized Radio text problem """ @@ -1867,13 +1013,6 @@ class RadioTextProblemTypeTestNonRandomized(RadioTextProblemTypeBase, NonRandomi ) -class RadioTextProblemTypeNeverShowCorrectnessTest(RadioTextProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Radio + Text Problem Type problems. - """ - pass - - class CheckboxTextProblemTypeBase(ChoiceTextProblemTypeTestBase): """ ProblemTypeTestBase specialization for Checkbox Text Problem Type @@ -1909,14 +1048,14 @@ class CheckboxTextProblemTypeBase(ChoiceTextProblemTypeTestBase): }) -class CheckboxTextProblemTypeTest(CheckboxTextProblemTypeBase, ProblemTypeTestMixin): +class CheckboxTextProblemTypeTest(CheckboxTextProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Checkbox Text Problem Type """ pass -class CheckboxTextProblemTypeTestNonRandomized(CheckboxTextProblemTypeBase, NonRandomizedProblemTypeTestMixin): +class CheckboxTextProblemTypeTestNonRandomized(CheckboxTextProblemTypeBase, ProblemTypeA11yTestMixin): """ Tests for non-randomized Checkbox problem """ @@ -1934,13 +1073,6 @@ class CheckboxTextProblemTypeTestNonRandomized(CheckboxTextProblemTypeBase, NonR ) -class CheckboxTextProblemTypeNeverShowCorrectnessTest(CheckboxTextProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Checkbox + Text Problem Type problems. - """ - pass - - class SymbolicProblemTypeBase(ProblemTypeTestBase): """ ProblemTypeTestBase specialization for Symbolic Problem Type @@ -1971,15 +1103,8 @@ class SymbolicProblemTypeBase(ProblemTypeTestBase): self.problem_page.fill_answer(choice) -class SymbolicProblemTypeTest(SymbolicProblemTypeBase, ProblemTypeTestMixin): +class SymbolicProblemTypeTest(SymbolicProblemTypeBase, ProblemTypeA11yTestMixin): """ Standard tests for the Symbolic Problem Type """ pass - - -class SymbolicProblemTypeNeverShowCorrectnessTest(SymbolicProblemTypeBase, ProblemNeverShowCorrectnessMixin): - """ - Ensure that correctness can be withheld for Symbolic Problem Type problems. - """ - pass diff --git a/common/test/acceptance/tests/lms/test_programs.py b/common/test/acceptance/tests/lms/test_programs.py index ecd0c55b72..ade51560f1 100644 --- a/common/test/acceptance/tests/lms/test_programs.py +++ b/common/test/acceptance/tests/lms/test_programs.py @@ -74,30 +74,6 @@ class ProgramPageBase(ProgramsConfigMixin, CatalogIntegrationMixin, UniqueCourse cache_programs_page.visit() -class ProgramListingPageTest(ProgramPageBase): - """Verify user-facing behavior of the program listing page.""" - shard = 21 - - def setUp(self): - super(ProgramListingPageTest, self).setUp() - - self.listing_page = ProgramListingPage(self.browser) - - def test_no_programs(self): - """ - Verify that no cards appear when the user has enrollments - but none are included in an active program. - """ - self.auth() - self.stub_catalog_api(self.programs, self.pathways) - self.cache_programs() - - self.listing_page.visit() - - self.assertTrue(self.listing_page.is_sidebar_present) - self.assertFalse(self.listing_page.are_cards_present) - - class ProgramListingPageA11yTest(ProgramPageBase): """Test program listing page accessibility.""" a11y = True diff --git a/common/test/acceptance/tests/lms/test_progress_page.py b/common/test/acceptance/tests/lms/test_progress_page.py index 5744800cff..118645dd01 100644 --- a/common/test/acceptance/tests/lms/test_progress_page.py +++ b/common/test/acceptance/tests/lms/test_progress_page.py @@ -7,24 +7,18 @@ progress page. from contextlib import contextmanager -import ddt from six.moves import range from ...fixtures.course import CourseFixture, XBlockFixtureDesc from ...pages.common.logout import LogoutPage from ...pages.lms.courseware import CoursewarePage -from ...pages.lms.instructor_dashboard import InstructorDashboardPage, StudentSpecificAdmin from ...pages.lms.problem import ProblemPage from ...pages.lms.progress import ProgressPage from ...pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage -from ...pages.studio.utils import type_in_codemirror -from ...pages.studio.xblock_editor import XBlockEditorView from ..helpers import ( UniqueCourseTest, auto_auth, create_multiple_choice_problem, - create_multiple_choice_xml, - get_modal_alert ) @@ -128,154 +122,6 @@ class ProgressPageBaseTest(UniqueCourseTest): self.logout_page.visit() -@ddt.ddt -class PersistentGradesTest(ProgressPageBaseTest): - """ - Test that grades for completed assessments are persisted - when various edits are made. - """ - shard = 22 - - def setUp(self): - super(PersistentGradesTest, self).setUp() - self.instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - - def _change_subsection_structure(self): - """ - Adds a unit to the subsection, which - should not affect a persisted subsection grade. - """ - self.studio_course_outline.visit() - subsection = self.studio_course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME) - subsection.expand_subsection() - subsection.add_unit() - self.studio_course_outline.wait_for_ajax() - subsection.publish() - - def _set_staff_lock_on_subsection(self, locked): - """ - Sets staff lock for a subsection, which should hide the - subsection score from students on the progress page. - """ - self.studio_course_outline.visit() - subsection = self.studio_course_outline.section_at(0).subsection_at(0) - subsection.set_staff_lock(locked) - self.assertEqual(subsection.has_staff_lock_warning, locked) - - def _get_problem_in_studio(self): - """ - Returns the editable problem component in studio, - along with its container unit, so any changes can - be published. - """ - self.studio_course_outline.visit() - self.studio_course_outline.section_at(0).subsection_at(0).expand_subsection() - unit = self.studio_course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to() - component = unit.xblocks[1] - return unit, component - - def _change_weight_for_problem(self): - """ - Changes the weight of the problem, which should not affect - persisted grades. - """ - unit, component = self._get_problem_in_studio() - component.edit() - component_editor = XBlockEditorView(self.browser, component.locator) - component_editor.set_field_value_and_save('Problem Weight', 5) - unit.publish() - - def _change_correct_answer_for_problem(self, new_correct_choice=1): - """ - Changes the correct answer of the problem. - """ - unit, component = self._get_problem_in_studio() - modal = component.edit() - - modified_content = create_multiple_choice_xml(correct_choice=new_correct_choice) - - type_in_codemirror(self, 0, modified_content) - modal.q(css='.action-save').click() - unit.publish() - - def _student_admin_action_for_problem(self, action_button, has_cancellable_alert=False): - """ - As staff, clicks the "delete student state" button, - deleting the student user's state for the problem. - """ - self.instructor_dashboard_page.visit() - student_admin_section = self.instructor_dashboard_page.select_student_admin(StudentSpecificAdmin) - student_admin_section.set_student_email_or_username(self.USERNAME) - student_admin_section.set_problem_location(self.problem1.locator) - getattr(student_admin_section, action_button).click() - if has_cancellable_alert: - alert = get_modal_alert(student_admin_section.browser) - alert.accept() - alert = get_modal_alert(student_admin_section.browser) - alert.dismiss() - return student_admin_section - - def test_progress_page_shows_scored_problems(self): - """ - Checks the progress page before and after answering - the course's first problem correctly. - """ - with self._logged_in_session(): - self.assertEqual(self._get_problem_scores(), [(0, 1), (0, 1)]) - self.assertEqual(self._get_section_score(), (0, 2)) - self.courseware_page.visit() - self._answer_problem_correctly() - self.assertEqual(self._get_problem_scores(), [(1, 1), (0, 1)]) - self.assertEqual(self._get_section_score(), (1, 2)) - - @ddt.data( - _change_subsection_structure, - _change_weight_for_problem - ) - def test_content_changes_do_not_change_score(self, edit): - with self._logged_in_session(): - self.courseware_page.visit() - self._answer_problem_correctly() - - with self._logged_in_session(staff=True): - edit(self) - - with self._logged_in_session(): - self.assertEqual(self._get_problem_scores(), [(1, 1), (0, 1)]) - self.assertEqual(self._get_section_score(), (1, 2)) - - def test_visibility_change_affects_score(self): - with self._logged_in_session(): - self.courseware_page.visit() - self._answer_problem_correctly() - - with self._logged_in_session(staff=True): - self._set_staff_lock_on_subsection(True) - - with self._logged_in_session(): - self.assertEqual(self._get_problem_scores(), None) - self.assertEqual(self._get_section_score(), None) - - with self._logged_in_session(staff=True): - self._set_staff_lock_on_subsection(False) - - with self._logged_in_session(): - self.assertEqual(self._get_problem_scores(), [(1, 1), (0, 1)]) - self.assertEqual(self._get_section_score(), (1, 2)) - - def test_delete_student_state_affects_score(self): - with self._logged_in_session(): - self.courseware_page.visit() - self._answer_problem_correctly() - - with self._logged_in_session(staff=True): - self._student_admin_action_for_problem('delete_state_button', has_cancellable_alert=True) - - with self._logged_in_session(): - self.assertEqual(self._get_problem_scores(), [(0, 1), (0, 1)]) - self.assertEqual(self._get_section_score(), (0, 2)) - - class SubsectionGradingPolicyBase(ProgressPageBaseTest): """ Base class for testing a subsection and its impact to diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py deleted file mode 100644 index 95b0282465..0000000000 --- a/common/test/acceptance/tests/lms/test_teams.py +++ /dev/null @@ -1,1987 +0,0 @@ -""" -Acceptance tests for the teams feature. -""" - - -import json -import random -import time -from uuid import uuid4 - -import ddt -from dateutil.parser import parse -from selenium.common.exceptions import TimeoutException -from six.moves import map, range - -from common.test.acceptance.fixtures import LMS_BASE_URL -from common.test.acceptance.fixtures.course import CourseFixture -from common.test.acceptance.fixtures.discussion import ForumsConfigMixin, MultipleThreadFixture, Thread -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.utils import confirm_prompt -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.lms.learner_profile import LearnerProfilePage -from common.test.acceptance.pages.lms.tab_nav import TabNavPage -from common.test.acceptance.pages.lms.teams import ( - BrowseTeamsPage, - BrowseTopicsPage, - EditMembershipPage, - MyTeamsPage, - TeamManagementPage, - TeamPage, - TeamsPage -) -from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest, get_modal_alert -from openedx.core.lib.tests import attr - -TOPICS_PER_PAGE = 12 - - -class TeamsTabBase(EventsTestMixin, ForumsConfigMixin, UniqueCourseTest): - """Base class for Teams Tab tests""" - def setUp(self): - super(TeamsTabBase, self).setUp() - self.tab_nav = TabNavPage(self.browser) - self.course_home_page = CourseHomePage(self.browser, self.course_id) - self.teams_page = TeamsPage(self.browser, self.course_id) - # TODO: Refactor so resetting events database is not necessary - self.reset_event_tracking() - - self.enable_forums() - - def create_topics(self, num_topics): - """Create `num_topics` test topics.""" - return [{u"description": i, u"name": i, u"id": i} for i in map(str, range(num_topics))] - - def create_teams(self, topic, num_teams, time_between_creation=0): - """Create `num_teams` teams belonging to `topic`.""" - teams = [] - for i in range(num_teams): - team = { - 'course_id': self.course_id, - 'topic_id': topic['id'], - 'name': u'Team {}'.format(i), - 'description': u'Description {}'.format(i), - 'language': 'aa', - 'country': 'AF' - } - teams.append(self.post_team_data(team)) - # Sadly, this sleep is necessary in order to ensure that - # sorting by last_activity_at works correctly when running - # in Jenkins. - # THIS IS AN ANTI-PATTERN - DO NOT COPY. - time.sleep(time_between_creation) - return teams - - def post_team_data(self, team_data): - """Given a JSON representation of a team, post it to the server.""" - response = self.course_fixture.session.post( - LMS_BASE_URL + '/api/team/v0/teams/', - data=json.dumps(team_data), - headers=self.course_fixture.headers - ) - self.assertEqual(response.status_code, 200) - return json.loads(response.text) - - def create_memberships(self, num_memberships, team_id): - """Create `num_memberships` users and assign them to `team_id`. The - last user created becomes the current user.""" - memberships = [] - for __ in range(num_memberships): - user_info = AutoAuthPage(self.browser, course_id=self.course_id).visit().user_info - memberships.append(user_info) - self.create_membership(user_info['username'], team_id) - #pylint: disable=attribute-defined-outside-init - self.user_info = memberships[-1] - return memberships - - def create_membership(self, username, team_id): - """Assign `username` to `team_id`.""" - response = self.course_fixture.session.post( - LMS_BASE_URL + '/api/team/v0/team_membership/', - data=json.dumps({'username': username, 'team_id': team_id}), - headers=self.course_fixture.headers - ) - return json.loads(response.text) - - def set_team_configuration(self, configuration_data, enroll_in_course=True, global_staff=False): - """ - Sets team configuration on the course and calls auto-auth on the user. - """ - #pylint: disable=attribute-defined-outside-init - self.course_fixture = CourseFixture(**self.course_info) - if configuration_data: - self.course_fixture.add_advanced_settings( - {u"teams_configuration": {u"value": configuration_data}} - ) - self.course_fixture.install() - - enroll_course_id = self.course_id if enroll_in_course else None - #pylint: disable=attribute-defined-outside-init - self.user_info = AutoAuthPage(self.browser, course_id=enroll_course_id, staff=global_staff).visit().user_info - self.course_home_page.visit() - - def verify_teams_present(self, present): - """ - Verifies whether or not the teams tab is present. If it should be present, also - checks the text on the page (to ensure view is working). - """ - if present: - self.assertIn("Teams", self.tab_nav.tab_names) - self.teams_page.visit() - self.assertEqual(self.teams_page.active_tab(), 'browse') - else: - self.assertNotIn("Teams", self.tab_nav.tab_names) - - def verify_teams(self, page, expected_teams): - """Verify that the list of team cards on the current page match the expected teams in order.""" - - def assert_team_equal(expected_team, team_card_name, team_card_description): - """ - Helper to assert that a single team card has the expected name and - description. - """ - self.assertEqual(expected_team['name'], team_card_name) - self.assertEqual(expected_team['description'], team_card_description) - - team_card_names = page.team_names - team_card_descriptions = page.team_descriptions - list(map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions)) - - def verify_my_team_count(self, expected_number_of_teams): - """ Verify the number of teams shown on "My Team". """ - - # We are doing these operations on this top-level page object to avoid reloading the page. - self.teams_page.verify_my_team_count(expected_number_of_teams) - - def only_team_events(self, event): - """Filter out all non-team events.""" - return event['event_type'].startswith('edx.team.') - - -@ddt.ddt -@attr(shard=5) -class TeamsTabTest(TeamsTabBase): - """ - Tests verifying when the Teams tab is present. - """ - def test_teams_not_enabled(self): - """ - Scenario: teams tab should not be present if no team configuration is set - Given I am enrolled in a course without team configuration - When I view the course info page - Then I should not see the Teams tab - """ - self.set_team_configuration(None) - self.verify_teams_present(False) - - def test_teams_not_enabled_no_topics(self): - """ - Scenario: teams tab should not be present if team configuration does not specify topics - Given I am enrolled in a course with no topics in the team configuration - When I view the course info page - Then I should not see the Teams tab - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": []}) - self.verify_teams_present(False) - - def test_teams_enabled(self): - """ - Scenario: teams tab should be present if user is enrolled in the course and it has team configuration - Given I am enrolled in a course with team configuration and topics - When I view the course info page - Then I should see the Teams tab - And the correct content should be on the page - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(1)}) - self.verify_teams_present(True) - - def test_teams_enabled_global_staff(self): - """ - Scenario: teams tab should be present if user is not enrolled in the course, but is global staff - Given there is a course with team configuration - And I am not enrolled in that course, but am global staff - When I view the course info page - Then I should see the Teams tab - And the correct content should be on the page - """ - self.set_team_configuration( - {u"max_team_size": 10, u"topics": self.create_topics(1)}, - enroll_in_course=False, - global_staff=True - ) - self.verify_teams_present(True) - - @ddt.data( - 'topics/{topic_id}', - 'topics/{topic_id}/search', - 'teams/{topic_id}/{team_id}/edit-team', - 'teams/{topic_id}/{team_id}' - ) - def test_unauthorized_error_message(self, route): - """Ensure that an error message is shown to the user if they attempt - to take an action which makes an AJAX request while not signed - in. - """ - topics = self.create_topics(1) - topic = topics[0] - self.set_team_configuration( - {u'max_team_size': 10, u'topics': topics}, - global_staff=True - ) - team = self.create_teams(topic, 1)[0] - self.teams_page.visit() - self.browser.delete_cookie('sessionid') - url = self.browser.current_url.split('#')[0] - self.browser.get( - '{url}#{route}'.format( - url=url, - route=route.format( - topic_id=topic['id'], - team_id=team['id'] - ) - ) - ) - self.teams_page.wait_for_ajax() - self.assertEqual( - self.teams_page.warning_message, - u"Your request could not be completed. Reload the page and try again." - ) - - @ddt.data( - ('browse', '.topics-list'), - # TODO: find a reliable way to match the "My Teams" tab - # ('my-teams', 'div.teams-list'), - ('teams/{topic_id}/{team_id}', 'div.discussion-module'), - ('topics/{topic_id}/create-team', 'div.create-team-instructions'), - ('topics/{topic_id}', '.teams-list'), - ('not-a-real-route', 'div.warning') - ) - @ddt.unpack - def test_url_routing(self, route, selector): - """Ensure that navigating to a URL route correctly updates the page - content. - """ - topics = self.create_topics(1) - topic = topics[0] - self.set_team_configuration({ - u'max_team_size': 10, - u'topics': topics - }) - team = self.create_teams(topic, 1)[0] - self.teams_page.visit() - - # Get the base URL (the URL without any trailing fragment) - url = self.browser.current_url - fragment_index = url.find('#') - if fragment_index >= 0: - url = url[0:fragment_index] - - self.browser.get( - '{url}#{route}'.format( - url=url, - route=route.format( - topic_id=topic['id'], - team_id=team['id'] - )) - ) - self.teams_page.wait_for_page() - self.teams_page.wait_for_ajax() - self.assertTrue(self.teams_page.q(css=selector).present) - self.assertTrue(self.teams_page.q(css=selector).visible) - - -@attr(shard=5) -class MyTeamsTest(TeamsTabBase): - """ - Tests for the "My Teams" tab of the Teams page. - """ - - def setUp(self): - super(MyTeamsTest, self).setUp() - self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} - self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) - self.my_teams_page = MyTeamsPage(self.browser, self.course_id) - self.page_viewed_event = { - 'event_type': 'edx.team.page_viewed', - 'event': { - 'page_name': 'my-teams', - 'topic_id': None, - 'team_id': None - } - } - - def test_not_member_of_any_teams(self): - """ - Scenario: Visiting the My Teams page when user is not a member of any team should not display any teams. - Given I am enrolled in a course with a team configuration and a topic but am not a member of a team - When I visit the My Teams page - And I should see no teams - And I should see a message that I belong to no teams. - """ - with self.assert_events_match_during(self.only_team_events, expected_events=[self.page_viewed_event]): - self.my_teams_page.visit() - self.assertEqual(len(self.my_teams_page.team_cards), 0, msg='Expected to see no team cards') - self.assertEqual( - self.my_teams_page.q(css='.page-content-main').text, - [u'You are not currently a member of any team.'] - ) - - def test_member_of_a_team(self): - """ - Scenario: Visiting the My Teams page when user is a member of a team should display the teams. - Given I am enrolled in a course with a team configuration and a topic and am a member of a team - When I visit the My Teams page - Then I should see a pagination header showing the number of teams - And I should see all the expected team cards - And I should not see a pagination footer - """ - teams = self.create_teams(self.topic, 1) - self.create_membership(self.user_info['username'], teams[0]['id']) - with self.assert_events_match_during(self.only_team_events, expected_events=[self.page_viewed_event]): - self.my_teams_page.visit() - self.verify_teams(self.my_teams_page, teams) - - def test_multiple_team_members(self): - """ - Scenario: Visiting the My Teams page when user is a member of a team should display the teams. - Given I am a member of a team with multiple members - When I visit the My Teams page - Then I should see the correct number of team members on my membership - """ - teams = self.create_teams(self.topic, 1) - self.create_memberships(4, teams[0]['id']) - self.my_teams_page.visit() - self.assertEqual(self.my_teams_page.team_memberships[0], '4 / 10 Members') - - -@attr(shard=5) -@ddt.ddt -class BrowseTopicsTest(TeamsTabBase): - """ - Tests for the Browse tab of the Teams page. - """ - - def setUp(self): - super(BrowseTopicsTest, self).setUp() - self.topics_page = BrowseTopicsPage(self.browser, self.course_id) - - @ddt.data(('name', False), ('team_count', True)) - @ddt.unpack - def test_sort_topics(self, sort_order, reverse): - """ - Scenario: the user should be able to sort the list of topics by name or team count - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics - Then I should see a list of topics for the course - When I choose a sort order - Then I should see the paginated list of topics in that order - """ - topics = self.create_topics(TOPICS_PER_PAGE + 1) - self.set_team_configuration({u"max_team_size": 100, u"topics": topics}) - for i, topic in enumerate(random.sample(topics, len(topics))): - self.create_teams(topic, i) - topic['team_count'] = i - self.topics_page.visit() - self.topics_page.sort_topics_by(sort_order) - topic_names = self.topics_page.topic_names - self.assertEqual(len(topic_names), TOPICS_PER_PAGE) - self.assertEqual( - topic_names, - [t['name'] for t in sorted(topics, key=lambda t: t[sort_order], reverse=reverse)][:TOPICS_PER_PAGE] - ) - - def test_sort_topics_update(self): - """ - Scenario: the list of topics should remain sorted after updates - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics and choose a sort order - Then I should see the paginated list of topics in that order - When I create a team in one of those topics - And I return to the topics list - Then I should see the topics in the correct sorted order - """ - topics = self.create_topics(3) - self.set_team_configuration({u"max_team_size": 100, u"topics": topics}) - self.topics_page.visit() - self.topics_page.sort_topics_by('team_count') - topic_name = self.topics_page.topic_names[-1] - topic = [t for t in topics if t['name'] == topic_name][0] - self.topics_page.browse_teams_for_topic(topic_name) - browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, topic) - browse_teams_page.wait_for_page() - browse_teams_page.click_create_team_link() - create_team_page = TeamManagementPage(self.browser, self.course_id, topic) - create_team_page.create_team() - - team_page = TeamPage(self.browser, self.course_id) - team_page.wait_for_page() - - team_page.click_all_topics() - self.topics_page.wait_for_page() - self.topics_page.wait_for_ajax() - self.assertEqual(topic_name, self.topics_page.topic_names[0]) - - def test_list_topics(self): - """ - Scenario: a list of topics should be visible in the "Browse" tab - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics - Then I should see a list of topics for the course - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(2)}) - self.topics_page.visit() - self.assertEqual(len(self.topics_page.topic_cards), 2) - self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 1-2 out of 2 total')) - self.assertFalse(self.topics_page.pagination_controls_visible()) - self.assertFalse(self.topics_page.is_previous_page_button_enabled()) - self.assertFalse(self.topics_page.is_next_page_button_enabled()) - - def test_topic_pagination(self): - """ - Scenario: a list of topics should be visible in the "Browse" tab, paginated 12 per page - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics - Then I should see only the first 12 topics - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(20)}) - self.topics_page.visit() - self.assertEqual(len(self.topics_page.topic_cards), TOPICS_PER_PAGE) - self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 1-12 out of 20 total')) - self.assertTrue(self.topics_page.pagination_controls_visible()) - self.assertFalse(self.topics_page.is_previous_page_button_enabled()) - self.assertTrue(self.topics_page.is_next_page_button_enabled()) - - def test_go_to_numbered_page(self): - """ - Scenario: topics should be able to be navigated by page number - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics - And I enter a valid page number in the page number input - Then I should see that page of topics - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(25)}) - self.topics_page.visit() - self.topics_page.go_to_page(3) - self.assertEqual(len(self.topics_page.topic_cards), 1) - self.assertTrue(self.topics_page.is_previous_page_button_enabled()) - self.assertFalse(self.topics_page.is_next_page_button_enabled()) - - def test_go_to_invalid_page(self): - """ - Scenario: browsing topics should not respond to invalid page numbers - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics - And I enter an invalid page number in the page number input - Then I should stay on the current page - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(13)}) - self.topics_page.visit() - self.topics_page.go_to_page(3) - self.assertEqual(self.topics_page.get_current_page_number(), 1) - - def test_page_navigation_buttons(self): - """ - Scenario: browsing topics should not respond to invalid page numbers - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics - When I press the next page button - Then I should move to the next page - When I press the previous page button - Then I should move to the previous page - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(13)}) - self.topics_page.visit() - self.topics_page.press_next_page_button() - self.assertEqual(len(self.topics_page.topic_cards), 1) - self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 13-13 out of 13 total')) - self.topics_page.press_previous_page_button() - self.assertEqual(len(self.topics_page.topic_cards), TOPICS_PER_PAGE) - self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 1-12 out of 13 total')) - - def test_topic_pagination_one_page(self): - """ - Scenario: Browsing topics when there are fewer topics than the page size i.e. 12 - all topics should show on one page - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse topics - And I should see corrected number of topic cards - And I should see the correct page header - And I should not see a pagination footer - """ - self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(10)}) - self.topics_page.visit() - self.assertEqual(len(self.topics_page.topic_cards), 10) - self.assertTrue(self.topics_page.get_pagination_header_text().startswith('Showing 1-10 out of 10 total')) - self.assertFalse(self.topics_page.pagination_controls_visible()) - - def test_topic_description_truncation(self): - """ - Scenario: excessively long topic descriptions should be truncated so - as to fit within a topic card. - Given I am enrolled in a course with a team configuration and a topic - with a long description - When I visit the Teams page - And I browse topics - Then I should see a truncated topic description - """ - initial_description = "A" + " really" * 50 + " long description" - self.set_team_configuration( - { - u"max_team_size": 1, - u"topics": [ - {"id": "long-description-topic", "description": initial_description}, - ] - } - ) - self.topics_page.visit() - truncated_description = self.topics_page.topic_descriptions[0] - self.assertLess(len(truncated_description), len(initial_description)) - self.assertTrue(truncated_description.endswith('...')) - self.assertIn(truncated_description.split('...')[0], initial_description) - - def test_go_to_teams_list(self): - """ - Scenario: Clicking on a Topic Card should take you to the - teams list for that Topic. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Teams page - And I browse topics - And I click on the arrow link to view teams for the first topic - Then I should be on the browse teams page - """ - topic = {u"name": u"Example Topic", u"id": u"example_topic", u"description": "Description"} - self.set_team_configuration( - {u"max_team_size": 1, u"topics": [topic]} - ) - self.topics_page.visit() - self.topics_page.browse_teams_for_topic('Example Topic') - browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, topic) - browse_teams_page.wait_for_page() - self.assertEqual(browse_teams_page.header_name, 'Example Topic') - self.assertEqual(browse_teams_page.header_description, 'Description') - - def test_page_viewed_event(self): - """ - Scenario: Visiting the browse topics page should fire a page viewed event. - Given I am enrolled in a course with a team configuration and a topic - When I visit the browse topics page - Then my browser should post a page viewed event - """ - topic = {u"name": u"Example Topic", u"id": u"example_topic", u"description": "Description"} - self.set_team_configuration( - {u"max_team_size": 1, u"topics": [topic]} - ) - events = [{ - 'event_type': 'edx.team.page_viewed', - 'event': { - 'page_name': 'browse', - 'topic_id': None, - 'team_id': None - } - }] - with self.assert_events_match_during(self.only_team_events, expected_events=events): - self.topics_page.visit() - - -@attr(shard=4) -@ddt.ddt -class BrowseTeamsWithinTopicTest(TeamsTabBase): - """ - Tests for browsing Teams within a Topic on the Teams page. - """ - TEAMS_PAGE_SIZE = 10 - - def setUp(self): - super(BrowseTeamsWithinTopicTest, self).setUp() - self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} - self.max_team_size = 10 - self.set_team_configuration({ - 'course_id': self.course_id, - 'max_team_size': self.max_team_size, - 'topics': [self.topic] - }) - self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) - self.topics_page = BrowseTopicsPage(self.browser, self.course_id) - - def teams_with_default_sort_order(self, teams): - """Return a list of teams sorted according to the default ordering - (last_activity_at, with a secondary sort by open slots). - """ - return sorted( - sorted(teams, key=lambda t: len(t['membership']), reverse=True), - key=lambda t: parse(t['last_activity_at']).replace(microsecond=0), - reverse=True - ) - - def verify_page_header(self): - """Verify that the page header correctly reflects the current topic's name and description.""" - self.assertEqual(self.browse_teams_page.header_name, self.topic['name']) - self.assertEqual(self.browse_teams_page.header_description, self.topic['description']) - - def verify_search_header(self, search_results_page, search_query): - """Verify that the page header correctly reflects the current topic's name and description.""" - self.assertEqual(search_results_page.header_name, 'Team Search') - self.assertEqual( - search_results_page.header_description, - u'Showing results for "{search_query}"'.format(search_query=search_query) - ) - - def verify_on_page(self, teams_page, page_num, total_teams, pagination_header_text, footer_visible): - """ - Verify that we are on the correct team list page. - - Arguments: - teams_page (BaseTeamsPage): The teams page object that should be the current page. - page_num (int): The one-indexed page number that we expect to be on - total_teams (list): An unsorted list of all the teams for the - current topic - pagination_header_text (str): Text we expect to see in the - pagination header. - footer_visible (bool): Whether we expect to see the pagination - footer controls. - """ - sorted_teams = self.teams_with_default_sort_order(total_teams) - self.assertTrue(teams_page.get_pagination_header_text().startswith(pagination_header_text)) - self.verify_teams( - teams_page, - sorted_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE] - ) - self.assertEqual( - teams_page.pagination_controls_visible(), - footer_visible, - msg='Expected paging footer to be ' + 'visible' if footer_visible else 'invisible' - ) - - @ddt.data( - ('open_slots', 'last_activity_at', True), - ('last_activity_at', 'open_slots', True) - ) - @ddt.unpack - def test_sort_teams(self, sort_order, secondary_sort_order, reverse): - """ - Scenario: the user should be able to sort the list of teams by open slots or last activity - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse teams within a topic - Then I should see a list of teams for that topic - When I choose a sort order - Then I should see the paginated list of teams in that order - """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) - for i, team in enumerate(random.sample(teams, len(teams))): - for _ in range(i): - user_info = AutoAuthPage(self.browser, course_id=self.course_id).visit().user_info - self.create_membership(user_info['username'], team['id']) - team['open_slots'] = self.max_team_size - i - - # Re-authenticate as staff after creating users - AutoAuthPage( - self.browser, - course_id=self.course_id, - staff=True - ).visit() - self.browse_teams_page.visit() - self.browse_teams_page.sort_teams_by(sort_order) - team_names = self.browse_teams_page.team_names - self.assertEqual(len(team_names), self.TEAMS_PAGE_SIZE) - sorted_teams = [ - team['name'] - for team in sorted( - sorted(teams, key=lambda t: t[secondary_sort_order], reverse=reverse), - key=lambda t: t[sort_order], - reverse=reverse - ) - ][:self.TEAMS_PAGE_SIZE] - self.assertEqual(team_names, sorted_teams) - - def test_default_sort_order(self): - """ - Scenario: the list of teams should be sorted by last activity by default - Given I am enrolled in a course with team configuration and topics - When I visit the Teams page - And I browse teams within a topic - Then I should see a list of teams for that topic, sorted by last activity - """ - self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) - self.browse_teams_page.visit() - self.assertEqual(self.browse_teams_page.sort_order, 'last activity') - - def test_no_teams(self): - """ - Scenario: Visiting a topic with no teams should not display any teams. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Teams page for that topic - Then I should see the correct page header - And I should see a pagination header showing no teams - And I should see no teams - And I should see a button to add a team - And I should not see a pagination footer - """ - self.browse_teams_page.visit() - self.verify_page_header() - self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) - self.assertEqual(len(self.browse_teams_page.team_cards), 0, msg='Expected to see no team cards') - self.assertFalse( - self.browse_teams_page.pagination_controls_visible(), - msg='Expected paging footer to be invisible' - ) - - def test_teams_one_page(self): - """ - Scenario: Visiting a topic with fewer teams than the page size should - all those teams on one page. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Teams page for that topic - Then I should see the correct page header - And I should see a pagination header showing the number of teams - And I should see all the expected team cards - And I should see a button to add a team - And I should not see a pagination footer - """ - teams = self.teams_with_default_sort_order( - self.create_teams(self.topic, self.TEAMS_PAGE_SIZE, time_between_creation=1) - ) - self.browse_teams_page.visit() - self.verify_page_header() - self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 1-10 out of 10 total')) - self.verify_teams(self.browse_teams_page, teams) - self.assertFalse( - self.browse_teams_page.pagination_controls_visible(), - msg='Expected paging footer to be invisible' - ) - - def test_teams_navigation_buttons(self): - """ - Scenario: The user should be able to page through a topic's team list - using navigation buttons when it is longer than the page size. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Teams page for that topic - Then I should see the correct page header - And I should see that I am on the first page of results - When I click on the next page button - Then I should see that I am on the second page of results - And when I click on the previous page button - Then I should see that I am on the first page of results - """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1, time_between_creation=1) - self.browse_teams_page.visit() - self.verify_page_header() - self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 11 total', True) - self.browse_teams_page.press_next_page_button() - self.verify_on_page(self.browse_teams_page, 2, teams, 'Showing 11-11 out of 11 total', True) - self.browse_teams_page.press_previous_page_button() - self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 11 total', True) - - def test_teams_page_input(self): - """ - Scenario: The user should be able to page through a topic's team list - using the page input when it is longer than the page size. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Teams page for that topic - Then I should see the correct page header - And I should see that I am on the first page of results - When I input the second page - Then I should see that I am on the second page of results - When I input the first page - Then I should see that I am on the first page of results - """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 10, time_between_creation=1) - self.browse_teams_page.visit() - self.verify_page_header() - self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 20 total', True) - self.browse_teams_page.go_to_page(2) - self.verify_on_page(self.browse_teams_page, 2, teams, 'Showing 11-20 out of 20 total', True) - self.browse_teams_page.go_to_page(1) - self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 20 total', True) - - def test_browse_team_topics(self): - """ - Scenario: User should be able to navigate to "browse all teams" and "search team description" links. - Given I am enrolled in a course with teams enabled - When I visit the Teams page for a topic - Then I should see the correct page header - And I should see the link to "browse teams in other topics" - When I should navigate to that link - Then I should see the topic browse page - """ - self.browse_teams_page.visit() - self.verify_page_header() - - self.browse_teams_page.click_browse_all_teams_link() - self.topics_page.wait_for_page() - - def test_search(self): - """ - Scenario: User should be able to search for a team - Given I am enrolled in a course with teams enabled - When I visit the Teams page for that topic - And I search for 'banana' - Then I should see the search result page - And the search header should be shown - And 0 results should be shown - And my browser should fire a page viewed event for the search page - And a searched event should have been fired - """ - # Note: all searches will return 0 results with the mock search server - # used by Bok Choy. - search_text = 'banana' - self.create_teams(self.topic, 5) - self.browse_teams_page.visit() - events = [{ - 'event_type': 'edx.team.page_viewed', - 'event': { - 'page_name': 'search-teams', - 'topic_id': self.topic['id'], - 'team_id': None - } - }, { - 'event_type': 'edx.team.searched', - 'event': { - 'search_text': search_text, - 'topic_id': self.topic['id'], - 'number_of_results': 0 - } - }] - with self.assert_events_match_during(self.only_team_events, expected_events=events, in_order=False): - search_results_page = self.browse_teams_page.search(search_text) - self.verify_search_header(search_results_page, search_text) - self.assertTrue(search_results_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) - - def test_page_viewed_event(self): - """ - Scenario: Visiting the browse page should fire a page viewed event. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Teams page - Then my browser should post a page viewed event for the teams page - """ - self.create_teams(self.topic, 5) - events = [{ - 'event_type': 'edx.team.page_viewed', - 'event': { - 'page_name': 'single-topic', - 'topic_id': self.topic['id'], - 'team_id': None - } - }] - with self.assert_events_match_during(self.only_team_events, expected_events=events): - self.browse_teams_page.visit() - - def test_team_name_xss(self): - """ - Scenario: Team names should be HTML-escaped on the teams page - Given I am enrolled in a course with teams enabled - When I visit the Teams page for a topic, with a team name containing JS code - Then I should not see any alerts - """ - self.post_team_data({ - 'course_id': self.course_id, - 'topic_id': self.topic['id'], - 'name': '', - 'description': 'Description', - 'language': 'aa', - 'country': 'AF' - }) - with self.assertRaises(TimeoutException): - self.browser.get(self.browse_teams_page.url) - alert = get_modal_alert(self.browser) - alert.accept() - - -class TeamFormActions(TeamsTabBase): - """ - Base class for create, edit, and delete team. - """ - TEAM_DESCRIPTION = 'The Avengers are a fictional team of superheroes.' - - topic = {'name': 'Example Topic', 'id': 'example_topic', 'description': 'Description'} - TEAMS_NAME = 'Avengers' - - def setUp(self): - super(TeamFormActions, self).setUp() - self.team_management_page = TeamManagementPage(self.browser, self.course_id, self.topic) - - def verify_page_header(self, title, description, breadcrumbs): - """ - Verify that the page header correctly reflects the - create team header, description and breadcrumb. - """ - self.assertEqual(self.team_management_page.header_page_name, title) - self.assertEqual(self.team_management_page.header_page_description, description) - self.assertEqual(self.team_management_page.header_page_breadcrumbs, breadcrumbs) - - def verify_and_navigate_to_create_team_page(self): - """Navigates to the create team page and verifies.""" - self.browse_teams_page.click_create_team_link() - self.verify_page_header( - title='Create a New Team', - description='Create a new team if you can\'t find an existing team to join, ' - 'or if you would like to learn with friends you know.', - breadcrumbs=u'All Topics {topic_name}'.format(topic_name=self.topic['name']) - ) - - def verify_and_navigate_to_edit_team_page(self): - """Navigates to the edit team page and verifies.""" - self.assertEqual(self.team_page.team_name, self.team['name']) - self.assertTrue(self.team_page.edit_team_button_present) - - self.team_page.click_edit_team_button() - - self.team_management_page.wait_for_page() - - # Edit page header. - self.verify_page_header( - title='Edit Team', - description='If you make significant changes, make sure you notify ' - 'members of the team before making these changes.', - breadcrumbs=u'All Topics {topic_name} {team_name}'.format( - topic_name=self.topic['name'], - team_name=self.team['name'] - ) - ) - - def verify_team_info(self, name, description, location, language): - """Verify the team information on team page.""" - self.assertEqual(self.team_page.team_name, name) - self.assertEqual(self.team_page.team_description, description) - self.assertEqual(self.team_page.team_location, location) - self.assertEqual(self.team_page.team_language, language) - - def fill_create_or_edit_form(self): - """Fill the create/edit team form fields with appropriate values.""" - self.team_management_page.value_for_text_field( - field_id='name', - value=self.TEAMS_NAME, - press_enter=False - ) - self.team_management_page.set_value_for_textarea_field( - field_id='description', - value=self.TEAM_DESCRIPTION - ) - self.team_management_page.value_for_dropdown_field(field_id='language', value='English') - self.team_management_page.value_for_dropdown_field(field_id='country', value='Pakistan') - - def verify_all_fields_exist(self): - """ - Verify the fields for create/edit page. - """ - self.assertEqual( - self.team_management_page.message_for_field('name'), - 'A name that identifies your team (maximum 255 characters).' - ) - self.assertEqual( - self.team_management_page.message_for_textarea_field('description'), - 'A short description of the team to help other learners understand ' - 'the goals or direction of the team (maximum 300 characters).' - ) - self.assertEqual( - self.team_management_page.message_for_field('country'), - 'The country that team members primarily identify with.' - ) - self.assertEqual( - self.team_management_page.message_for_field('language'), - 'The language that team members primarily use to communicate with each other.' - ) - - -@attr(shard=4) -@ddt.ddt -class CreateTeamTest(TeamFormActions): - """ - Tests for creating a new Team within a Topic on the Teams page. - """ - - def setUp(self): - super(CreateTeamTest, self).setUp() - self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) - - self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) - self.browse_teams_page.visit() - - def test_user_can_see_create_team_page(self): - """ - Scenario: The user should be able to see the create team page via teams list page. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Teams page for that topic - Then I should see the Create Team page link on bottom - And When I click create team link - Then I should see the create team page. - And I should see the create team header - And I should also see the help messages for fields. - """ - self.verify_and_navigate_to_create_team_page() - self.verify_all_fields_exist() - - def test_user_can_see_error_message_for_missing_data(self): - """ - Scenario: The user should be able to see error message in case of missing required field. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Create Team page for that topic - Then I should see the Create Team header and form - And When I click create team button without filling required fields - Then I should see the error message and highlighted fields. - """ - self.verify_and_navigate_to_create_team_page() - - # `submit_form` clicks on a button, but that button doesn't always - # have the click event handler registered on it in time. That's why - # this test is flaky. Unfortunately, I don't know of a straightforward - # way to write something that waits for that event handler to be bound - # to the button element. So I used time.sleep as well, even though - # the bok choy docs explicitly ask us not to: - # https://bok-choy.readthedocs.io/en/latest/guidelines.html - # Sorry! For the story to address this anti-pattern, see TNL-5820 - time.sleep(0.5) - self.team_management_page.submit_form() - self.team_management_page.wait_for( - lambda: self.team_management_page.validation_message_text, - "Validation message text never loaded." - ) - self.assertEqual( - self.team_management_page.validation_message_text, - 'Check the highlighted fields below and try again.' - ) - self.assertTrue(self.team_management_page.error_for_field(field_id='name')) - self.assertTrue(self.team_management_page.error_for_field(field_id='description')) - - def test_user_can_see_error_message_for_incorrect_data(self): - """ - Scenario: The user should be able to see error message in case of increasing length for required fields. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Create Team page for that topic - Then I should see the Create Team header and form - When I add text > than 255 characters for name field - And I click Create button - Then I should see the error message for exceeding length. - """ - self.verify_and_navigate_to_create_team_page() - - # Fill the name field with >255 characters to see validation message. - self.team_management_page.value_for_text_field( - field_id='name', - value='EdX is a massive open online course (MOOC) provider and online learning platform. ' - 'It hosts online university-level courses in a wide range of disciplines to a worldwide ' - 'audience, some at no charge. It also conducts research into learning based on how ' - 'people use its platform. EdX was created for students and institutions that seek to' - 'transform themselves through cutting-edge technologies, innovative pedagogy, and ' - 'rigorous courses. More than 70 schools, nonprofits, corporations, and international' - 'organizations offer or plan to offer courses on the edX website. As of 22 October 2014,' - 'edX has more than 4 million users taking more than 500 courses online.', - press_enter=False - ) - self.team_management_page.submit_form() - - self.assertEqual( - self.team_management_page.validation_message_text, - 'Check the highlighted fields below and try again.' - ) - self.assertTrue(self.team_management_page.error_for_field(field_id='name')) - - def test_user_can_create_new_team_successfully(self): - """ - Scenario: The user should be able to create new team. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Create Team page for that topic - Then I should see the Create Team header and form - When I fill all the fields present with appropriate data - And I click Create button - Then I expect analytics events to be emitted - And I should see the page for my team - And I should see the message that says "You are member of this team" - And the new team should be added to the list of teams within the topic - And the number of teams should be updated on the topic card - And if I switch to "My Team", the newly created team is displayed - """ - AutoAuthPage(self.browser, course_id=self.course_id).visit() - self.browse_teams_page.visit() - - self.verify_and_navigate_to_create_team_page() - - self.fill_create_or_edit_form() - - expected_events = [ - { - 'event_type': 'edx.team.created' - }, - { - 'event_type': 'edx.team.learner_added', - 'event': { - 'add_method': 'added_on_create', - } - } - ] - with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): - self.team_management_page.submit_form() - - # Verify that the page is shown for the new team - team_page = TeamPage(self.browser, self.course_id) - team_page.wait_for_page() - self.assertEqual(team_page.team_name, self.TEAMS_NAME) - self.assertEqual(team_page.team_description, self.TEAM_DESCRIPTION) - self.assertEqual(team_page.team_user_membership_text, 'You are a member of this team.') - - # Verify the new team was added to the topic list - self.teams_page.click_specific_topic("Example Topic") - self.teams_page.verify_topic_team_count(1) - - self.teams_page.click_all_topics() - self.teams_page.verify_team_count_in_first_topic(1) - - # Verify that if one switches to "My Team" without reloading the page, the newly created team is shown. - self.verify_my_team_count(1) - - def test_user_can_cancel_the_team_creation(self): - """ - Scenario: The user should be able to cancel the creation of new team. - Given I am enrolled in a course with a team configuration and a topic - When I visit the Create Team page for that topic - Then I should see the Create Team header and form - When I click Cancel button - Then I should see teams list page without any new team. - And if I switch to "My Team", it shows no teams - """ - self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) - - self.verify_and_navigate_to_create_team_page() - - # We add a sleep here to allow time for the click event handler to bind - # to the cancel button. Using time.sleep in bok-choy tests is, - # generally, an anti-pattern. So don't copy this :). - # For the story to address this anti-pattern, see TNL-5820 - time.sleep(0.5) - - self.team_management_page.cancel_team() - - self.browse_teams_page.wait_for_page() - self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) - - self.teams_page.click_all_topics() - self.teams_page.verify_team_count_in_first_topic(0) - - self.verify_my_team_count(0) - - def test_page_viewed_event(self): - """ - Scenario: Visiting the create team page should fire a page viewed event. - Given I am enrolled in a course with a team configuration and a topic - When I visit the create team page - Then my browser should post a page viewed event - """ - events = [{ - 'event_type': 'edx.team.page_viewed', - 'event': { - 'page_name': 'new-team', - 'topic_id': self.topic['id'], - 'team_id': None - } - }] - with self.assert_events_match_during(self.only_team_events, expected_events=events): - self.verify_and_navigate_to_create_team_page() - - -@attr(shard=21) -@ddt.ddt -class DeleteTeamTest(TeamFormActions): - """ - Tests for deleting teams. - """ - - def setUp(self): - super(DeleteTeamTest, self).setUp() - - self.set_team_configuration( - {'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}, - global_staff=True - ) - - self.team = self.create_teams(self.topic, num_teams=1)[0] - self.team_page = TeamPage(self.browser, self.course_id, team=self.team) - - #need to have a membership to confirm it gets deleted as well - self.create_membership(self.user_info['username'], self.team['id']) - - self.team_page.visit() - - def test_cancel_delete(self): - """ - Scenario: The user should be able to cancel the Delete Team dialog - Given I am staff user for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - And When I click edit team button - Then I should see the Delete Team button - When I click the delete team button - And I cancel the prompt - And I refresh the page - Then I should still see the team - """ - self.delete_team(cancel=True) - self.team_management_page.wait_for_page() - self.browser.refresh() - self.team_management_page.wait_for_page() - self.assertEqual( - ' '.join(('All Topics', self.topic['name'], self.team['name'])), - self.team_management_page.header_page_breadcrumbs - ) - - @ddt.data('Moderator', 'Community TA', 'Administrator', None) - def test_delete_team(self, role): - """ - Scenario: The user should be able to see and navigate to the delete team page. - Given I am staff user for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - And When I click edit team button - Then I should see the Delete Team button - When I click the delete team button - And I confirm the prompt - Then I should see the browse teams page - And the team should not be present - """ - # If role is None, remain logged in as global staff - if role is not None: - AutoAuthPage( - self.browser, - course_id=self.course_id, - staff=False, - roles=role - ).visit() - self.team_page.visit() - self.delete_team(require_notification=False) - browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) - browse_teams_page.wait_for_page() - self.assertNotIn(self.team['name'], browse_teams_page.team_names) - - def delete_team(self, **kwargs): - """ - Delete a team. Passes `kwargs` to `confirm_prompt`. - Expects edx.team.deleted event to be emitted, with correct course_id. - Also expects edx.team.learner_removed event to be emitted for the - membership that is removed as a part of the delete operation. - """ - - self.team_page.click_edit_team_button() - self.team_management_page.wait_for_page() - self.team_management_page.delete_team_button.click() - - if 'cancel' in kwargs and kwargs['cancel'] is True: - confirm_prompt(self.team_management_page, **kwargs) - else: - expected_events = [ - { - 'event_type': 'edx.team.deleted', - 'event': { - 'team_id': self.team['id'] - } - }, - { - 'event_type': 'edx.team.learner_removed', - 'event': { - 'team_id': self.team['id'], - 'remove_method': 'team_deleted', - 'user_id': self.user_info['user_id'] - } - } - ] - with self.assert_events_match_during( - event_filter=self.only_team_events, expected_events=expected_events - ): - confirm_prompt(self.team_management_page, **kwargs) - - def test_delete_team_updates_topics(self): - """ - Scenario: Deleting a team should update the team count on the topics page - Given I am staff user for a course with a team - And I delete a team - When I navigate to the browse topics page - Then the team count for the deletd team's topic should be updated - """ - self.delete_team(require_notification=False) - BrowseTeamsPage(self.browser, self.course_id, self.topic).click_all_topics() - topics_page = BrowseTopicsPage(self.browser, self.course_id) - topics_page.wait_for_page() - self.teams_page.verify_topic_team_count(0) - - -@attr(shard=17) -@ddt.ddt -class EditTeamTest(TeamFormActions): - """ - Tests for editing the team. - """ - - def setUp(self): - super(EditTeamTest, self).setUp() - - self.set_team_configuration( - {'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}, - global_staff=True - ) - - self.team = self.create_teams(self.topic, num_teams=1)[0] - self.team_page = TeamPage(self.browser, self.course_id, team=self.team) - self.team_page.visit() - - def test_staff_can_navigate_to_edit_team_page(self): - """ - Scenario: The user should be able to see and navigate to the edit team page. - Given I am staff user for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - And When I click edit team button - Then I should see the edit team page - And I should see the edit team header - And I should also see the help messages for fields - """ - self.verify_and_navigate_to_edit_team_page() - self.verify_all_fields_exist() - - def test_staff_can_edit_team_successfully(self): - """ - Scenario: The staff should be able to edit team successfully. - Given I am staff user for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - And When I click edit team button - Then I should see the edit team page - And an analytics event should be fired - When I edit all the fields with appropriate data - And I click Update button - Then I should see the page for my team with updated data - """ - self.verify_team_info( - name=self.team['name'], - description=self.team['description'], - location='Afghanistan', - language='Afar' - ) - self.verify_and_navigate_to_edit_team_page() - - self.fill_create_or_edit_form() - - expected_events = [ - { - 'event_type': 'edx.team.changed', - 'event': { - 'team_id': self.team['id'], - 'field': 'country', - 'old': 'AF', - 'new': 'PK', - 'truncated': [], - } - }, - { - 'event_type': 'edx.team.changed', - 'event': { - 'team_id': self.team['id'], - 'field': 'name', - 'old': self.team['name'], - 'new': self.TEAMS_NAME, - 'truncated': [], - } - }, - { - 'event_type': 'edx.team.changed', - 'event': { - 'team_id': self.team['id'], - 'field': 'language', - 'old': 'aa', - 'new': 'en', - 'truncated': [], - } - }, - { - 'event_type': 'edx.team.changed', - 'event': { - 'team_id': self.team['id'], - 'field': 'description', - 'old': self.team['description'], - 'new': self.TEAM_DESCRIPTION, - 'truncated': [], - } - }, - ] - with self.assert_events_match_during( - event_filter=self.only_team_events, - expected_events=expected_events, - ): - self.team_management_page.submit_form() - - self.team_page.wait_for_page() - - self.verify_team_info( - name=self.TEAMS_NAME, - description=self.TEAM_DESCRIPTION, - location='Pakistan', - language='English' - ) - - def test_staff_can_cancel_the_team_edit(self): - """ - Scenario: The user should be able to cancel the editing of team. - Given I am staff user for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - And When I click edit team button - Then I should see the edit team page - Then I should see the Edit Team header - When I click Cancel button - Then I should see team page page without changes. - """ - self.verify_team_info( - name=self.team['name'], - description=self.team['description'], - location='Afghanistan', - language='Afar' - ) - - self.verify_and_navigate_to_edit_team_page() - - self.fill_create_or_edit_form() - self.team_management_page.cancel_team() - - self.team_page.wait_for_page() - - self.verify_team_info( - name=self.team['name'], - description=self.team['description'], - location='Afghanistan', - language='Afar' - ) - - def test_student_cannot_see_edit_button(self): - """ - Scenario: The student should not see the edit team button. - Given I am student for a course with a team - When I visit the Team profile page - Then I should not see the Edit Team button - """ - AutoAuthPage(self.browser, course_id=self.course_id).visit() - self.team_page.visit() - self.assertFalse(self.team_page.edit_team_button_present) - - @ddt.data('Moderator', 'Community TA', 'Administrator') - def test_discussion_privileged_user_can_edit_team(self, role): - """ - Scenario: The user with specified role should see the edit team button. - Given I am user with privileged role for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - """ - kwargs = { - 'course_id': self.course_id, - 'staff': False - } - if role is not None: - kwargs['roles'] = role - - AutoAuthPage(self.browser, **kwargs).visit() - - self.team_page.visit() - self.teams_page.wait_for_page() - self.assertTrue(self.team_page.edit_team_button_present) - - self.verify_team_info( - name=self.team['name'], - description=self.team['description'], - location='Afghanistan', - language='Afar' - ) - self.verify_and_navigate_to_edit_team_page() - - self.fill_create_or_edit_form() - self.team_management_page.submit_form() - - self.team_page.wait_for_page() - - self.verify_team_info( - name=self.TEAMS_NAME, - description=self.TEAM_DESCRIPTION, - location='Pakistan', - language='English' - ) - - def test_page_viewed_event(self): - """ - Scenario: Visiting the edit team page should fire a page viewed event. - Given I am enrolled in a course with a team configuration and a topic - When I visit the edit team page - Then my browser should post a page viewed event - """ - events = [{ - 'event_type': 'edx.team.page_viewed', - 'event': { - 'page_name': 'edit-team', - 'topic_id': self.topic['id'], - 'team_id': self.team['id'] - } - }] - with self.assert_events_match_during(self.only_team_events, expected_events=events): - self.verify_and_navigate_to_edit_team_page() - - -@attr(shard=17) -@ddt.ddt -class EditMembershipTest(TeamFormActions): - """ - Tests for administrating from the team membership page - """ - - def setUp(self): - super(EditMembershipTest, self).setUp() - - self.set_team_configuration( - {'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}, - global_staff=True - ) - self.team_management_page = TeamManagementPage(self.browser, self.course_id, self.topic) - self.team = self.create_teams(self.topic, num_teams=1)[0] - - #make sure a user exists on this team so we can edit the membership - self.create_membership(self.user_info['username'], self.team['id']) - - self.edit_membership_page = EditMembershipPage(self.browser, self.course_id, self.team) - self.team_page = TeamPage(self.browser, self.course_id, team=self.team) - - def edit_membership_helper(self, role, cancel=False): - """ - Helper for common functionality in edit membership tests. - Checks for all relevant assertions about membership being removed, - including verify edx.team.learner_removed events are emitted. - """ - if role is not None: - AutoAuthPage( - self.browser, - course_id=self.course_id, - staff=False, - roles=role - ).visit() - - self.team_page.visit() - self.team_page.click_edit_team_button() - self.team_management_page.wait_for_page() - - self.assertTrue( - self.team_management_page.membership_button_present - ) - - self.team_management_page.click_membership_button() - self.edit_membership_page.wait_for_page() - self.edit_membership_page.click_first_remove() - if cancel: - self.edit_membership_page.cancel_delete_membership_dialog() - self.assertEqual(self.edit_membership_page.team_members, 1) - else: - expected_events = [ - { - 'event_type': 'edx.team.learner_removed', - 'event': { - 'team_id': self.team['id'], - 'remove_method': 'removed_by_admin', - 'user_id': self.user_info['user_id'] - } - } - ] - with self.assert_events_match_during( - event_filter=self.only_team_events, expected_events=expected_events - ): - self.edit_membership_page.confirm_delete_membership_dialog() - self.assertEqual(self.edit_membership_page.team_members, 0) - self.edit_membership_page.wait_for_page() - - @ddt.data('Moderator', 'Community TA', 'Administrator', None) - def test_remove_membership(self, role): - """ - Scenario: The user should be able to remove a membership - Given I am staff user for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - And When I click edit team button - Then I should see the Edit Membership button - And When I click the edit membership button - Then I should see the edit membership page - And When I click the remove button and confirm the dialog - Then my membership should be removed, and I should remain on the page - """ - self.edit_membership_helper(role, cancel=False) - - @ddt.data('Moderator', 'Community TA', 'Administrator', None) - def test_cancel_remove_membership(self, role): - """ - Scenario: The user should be able to remove a membership - Given I am staff user for a course with a team - When I visit the Team profile page - Then I should see the Edit Team button - And When I click edit team button - Then I should see the Edit Membership button - And When I click the edit membership button - Then I should see the edit membership page - And When I click the remove button and cancel the dialog - Then my membership should not be removed, and I should remain on the page - """ - self.edit_membership_helper(role, cancel=True) - - -@attr(shard=17) -@ddt.ddt -class TeamPageTest(TeamsTabBase): - """Tests for viewing a specific team""" - - SEND_INVITE_TEXT = 'Send this link to friends so that they can join too.' - - def setUp(self): - super(TeamPageTest, self).setUp() - self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} - - def _set_team_configuration_and_membership( - self, - max_team_size=10, - membership_team_index=0, - visit_team_index=0, - create_membership=True, - another_user=False): - """ - Set team configuration. - - Arguments: - max_team_size (int): number of users a team can have - membership_team_index (int): index of team user will join - visit_team_index (int): index of team user will visit - create_membership (bool): whether to create membership or not - another_user (bool): another user to visit a team - """ - #pylint: disable=attribute-defined-outside-init - self.set_team_configuration( - {'course_id': self.course_id, 'max_team_size': max_team_size, 'topics': [self.topic]} - ) - self.teams = self.create_teams(self.topic, 2) - - if create_membership: - self.create_membership(self.user_info['username'], self.teams[membership_team_index]['id']) - - if another_user: - AutoAuthPage(self.browser, course_id=self.course_id).visit() - - self.team_page = TeamPage(self.browser, self.course_id, self.teams[visit_team_index]) - - def setup_thread(self): - """ - Create and return a thread for this test's discussion topic. - """ - thread = Thread( - id="test_thread_{}".format(uuid4().hex), - commentable_id=self.teams[0]['discussion_topic_id'], - body="Dummy text body.", - context="standalone", - ) - thread_fixture = MultipleThreadFixture([thread]) - thread_fixture.push() - return thread - - def setup_discussion_user(self, role=None, staff=False): - """Set this test's user to have the given role in its - discussions. Role is one of 'Community TA', 'Moderator', - 'Administrator', or 'Student'. - """ - kwargs = { - 'course_id': self.course_id, - 'staff': staff - } - if role is not None: - kwargs['roles'] = role - #pylint: disable=attribute-defined-outside-init - self.user_info = AutoAuthPage(self.browser, **kwargs).visit().user_info - - def verify_teams_discussion_permissions(self, should_have_permission): - """Verify that the teams discussion component is in the correct state - for the test user. If `should_have_permission` is True, assert that - the user can see controls for posting replies, voting, editing, and - deleting. Otherwise, assert that those controls are hidden. - """ - thread = self.setup_thread() - self.team_page.visit() - self.assertEqual(self.team_page.discussion_id, self.teams[0]['discussion_topic_id']) - discussion_page = self.team_page.discussion_page - discussion_page.wait_for_page() - self.assertTrue(discussion_page.is_discussion_expanded()) - self.assertEqual(discussion_page.get_num_displayed_threads(), 1) - discussion_page.show_thread(thread['id']) - thread_page = discussion_page.thread_page - assertion = self.assertTrue if should_have_permission else self.assertFalse - assertion(thread_page.q(css='.post-header-actions').present) - assertion(thread_page.q(css='.add-response').present) - - def test_discussion_on_my_team_page(self): - """ - Scenario: Team Page renders a discussion for a team to which I belong. - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic of which I am a member - When the team has a discussion with a thread - And I visit the Team page for that team - Then I should see a discussion with the correct discussion_id - And I should see the existing thread - And I should see controls to change the state of the discussion - """ - self._set_team_configuration_and_membership() - self.verify_teams_discussion_permissions(True) - - @ddt.data(True, False) - def test_discussion_on_other_team_page(self, is_staff): - """ - Scenario: Team Page renders a team discussion for a team to which I do - not belong. - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic of which I am not a member - When the team has a discussion with a thread - And I visit the Team page for that team - Then I should see a discussion with the correct discussion_id - And I should see the team's thread - And I should not see controls to change the state of the discussion - """ - self._set_team_configuration_and_membership(create_membership=False) - self.setup_discussion_user(staff=is_staff) - self.verify_teams_discussion_permissions(False) - - @ddt.data('Moderator', 'Community TA', 'Administrator') - def test_discussion_privileged(self, role): - self._set_team_configuration_and_membership(create_membership=False) - self.setup_discussion_user(role=role) - self.verify_teams_discussion_permissions(True) - - def assert_team_details(self, num_members, is_member=True, max_size=10): - """ - Verifies that user can see all the information, present on detail page according to their membership status. - - Arguments: - num_members (int): number of users in a team - is_member (bool) default True: True if request user is member else False - max_size (int): number of users a team can have - """ - self.assertEqual( - self.team_page.team_capacity_text, - self.team_page.format_capacity_text(num_members, max_size) - ) - self.assertEqual(self.team_page.team_location, 'Afghanistan') - self.assertEqual(self.team_page.team_language, 'Afar') - self.assertEqual(self.team_page.team_members, num_members) - - if num_members > 0: - self.assertTrue(self.team_page.team_members_present) - else: - self.assertFalse(self.team_page.team_members_present) - - if is_member: - self.assertEqual(self.team_page.team_user_membership_text, 'You are a member of this team.') - self.assertTrue(self.team_page.team_leave_link_present) - self.assertTrue(self.team_page.new_post_button_present) - else: - self.assertEqual(self.team_page.team_user_membership_text, '') - self.assertFalse(self.team_page.team_leave_link_present) - self.assertFalse(self.team_page.new_post_button_present) - - def test_team_member_can_see_full_team_details(self): - """ - Scenario: Team member can see full info for team. - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic of which I am a member - When I visit the Team page for that team - Then I should see the full team detail - And I should see the team members - And I should see my team membership text - And I should see the language & country - And I should see the Leave Team and Invite Team - """ - self._set_team_configuration_and_membership() - self.team_page.visit() - - self.assert_team_details( - num_members=1, - ) - - def test_other_users_can_see_limited_team_details(self): - """ - Scenario: Users who are not member of this team can only see limited info for this team. - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic of which I am not a member - When I visit the Team page for that team - Then I should not see full team detail - And I should see the team members - And I should not see my team membership text - And I should not see the Leave Team and Invite Team links - """ - self._set_team_configuration_and_membership(create_membership=False) - self.team_page.visit() - - self.assert_team_details(is_member=False, num_members=0) - - def test_user_can_navigate_to_members_profile_page(self): - """ - Scenario: User can navigate to profile page via team member profile image. - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic of which I am a member - When I visit the Team page for that team - Then I should see profile images for the team members - When I click on the first profile image - Then I should be taken to the user's profile page - And I should see the username on profile page - """ - self._set_team_configuration_and_membership() - self.team_page.visit() - - learner_name = self.team_page.first_member_username - - self.team_page.click_first_profile_image() - - learner_profile_page = LearnerProfilePage(self.browser, learner_name) - learner_profile_page.wait_for_page() - learner_profile_page.wait_for_field('username') - self.assertTrue(learner_profile_page.field_is_visible('username')) - - def test_join_team(self): - """ - Scenario: User can join a Team if not a member already.. - - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic - And I visit the Team page for that team - Then I should see Join Team button - And I should not see New Post button - When I click on Join Team button - Then there should be no Join Team button and no message - And an analytics event should be emitted - And I should see the updated information under Team Details - And I should see New Post button - And if I switch to "My Team", the team I have joined is displayed - """ - self._set_team_configuration_and_membership(create_membership=False) - teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) - teams_page.visit() - teams_page.view_first_team() - self.assertTrue(self.team_page.join_team_button_present) - expected_events = [ - { - 'event_type': 'edx.team.learner_added', - 'event': { - 'add_method': 'joined_from_team_view' - } - } - ] - with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): - self.team_page.click_join_team_button() - self.assertFalse(self.team_page.join_team_button_present) - self.assertFalse(self.team_page.join_team_message_present) - self.assert_team_details(num_members=1, is_member=True) - - # Verify that if one switches to "My Team" without reloading the page, the newly joined team is shown. - self.teams_page.click_all_topics() - self.verify_my_team_count(1) - - def test_already_member_message(self): - """ - Scenario: User should see `You are already in a team` if user is a - member of other team. - - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic - And I am already a member of a team - And I visit a team other than mine - Then I should see `You are already in a team` message - """ - self._set_team_configuration_and_membership(membership_team_index=0, visit_team_index=1) - self.team_page.visit() - self.assertEqual(self.team_page.join_team_message, 'You already belong to another team in this team set.') - self.assert_team_details(num_members=0, is_member=False) - - def test_team_full_message(self): - """ - Scenario: User should see `Team is full` message when team is full. - - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic - And team has no space left - And I am not a member of any team - And I visit the team - Then I should see `Team is full` message - """ - self._set_team_configuration_and_membership( - create_membership=True, - max_team_size=1, - membership_team_index=0, - visit_team_index=0, - another_user=True - ) - self.team_page.visit() - self.assertEqual(self.team_page.join_team_message, 'This team is full.') - self.assert_team_details(num_members=1, is_member=False, max_size=1) - - def test_leave_team(self): - """ - Scenario: User can leave a team. - - Given I am enrolled in a course with a team configuration, a topic, - and a team belonging to that topic - And I am a member of team - And I visit the team - And I should not see Join Team button - And I should see New Post button - Then I should see Leave Team link - When I click on Leave Team link - Then user should be removed from team - And an analytics event should be emitted - And I should see Join Team button - And I should not see New Post button - And if I switch to "My Team", the team I have left is not displayed - """ - self._set_team_configuration_and_membership() - self.team_page.visit() - self.assertFalse(self.team_page.join_team_button_present) - self.assert_team_details(num_members=1) - expected_events = [ - { - 'event_type': 'edx.team.learner_removed', - 'event': { - 'remove_method': 'self_removal' - } - } - ] - with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): - # I think we're seeing the same problem that we're seeing in - # CreateTeamTest.test_user_can_see_error_message_for_missing_data. - # We click on the "leave team" link after it's loaded, but before - # its JavaScript event handler is added. Adding this sleep gives - # enough time for that event handler to bind to the link. Sorry! - # For the story to address this anti-pattern, see TNL-5820 - time.sleep(0.5) - self.team_page.click_leave_team_link() - self.assert_team_details(num_members=0, is_member=False) - self.assertTrue(self.team_page.join_team_button_present) - - # Verify that if one switches to "My Team" without reloading the page, the old team no longer shows. - self.teams_page.click_all_topics() - self.verify_my_team_count(0) - - def test_page_viewed_event(self): - """ - Scenario: Visiting the team profile page should fire a page viewed event. - Given I am enrolled in a course with a team configuration and a topic - When I visit the team profile page - Then my browser should post a page viewed event - """ - self._set_team_configuration_and_membership() - events = [{ - 'event_type': 'edx.team.page_viewed', - 'event': { - 'page_name': 'single-team', - 'topic_id': self.topic['id'], - 'team_id': self.teams[0]['id'] - } - }] - with self.assert_events_match_during(self.only_team_events, expected_events=events): - self.team_page.visit() diff --git a/common/test/acceptance/tests/lms/test_unicode_username_admin.py b/common/test/acceptance/tests/lms/test_unicode_username_admin.py deleted file mode 100644 index 0885cd7b40..0000000000 --- a/common/test/acceptance/tests/lms/test_unicode_username_admin.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -""" -End-to-end tests for admin change view. -""" - - -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.admin import ChangeUserAdminPage -from common.test.acceptance.tests.helpers import AcceptanceTest - - -class UnicodeUsernameAdminTest(AcceptanceTest): - """ - Tests if it is possible to update users with unicode usernames in the admin. - """ - shard = 21 - - # The word below reads "Omar II", in Arabic. It also contains a space and - # an Eastern Arabic Number another option is to use the Esperanto fake - # language but this was used instead to test non-western letters. - FIXTURE_USERNAME = u'عمر ٢' - - # From the db fixture `unicode_user.json` - FIXTURE_USER_ID = 1000 - - def setUp(self): - """ - Initializes and visits the change user admin page as a superuser. - """ - # Some state is constructed by the parent setUp() routine - super(UnicodeUsernameAdminTest, self).setUp() - - AutoAuthPage(self.browser, staff=True, superuser=True).visit() - - # Load page objects for use by the tests - self.page = ChangeUserAdminPage(self.browser, self.FIXTURE_USER_ID) - - # Navigate to the index page and get testing! - self.page.visit() - - def test_update_first_name(self): - """ - As a superuser I should be able to update the first name of a user with unicode username. - """ - self.assertNotEqual(self.page.first_name, 'John') - self.assertEqual(self.page.username, self.FIXTURE_USERNAME) - - self.page.change_first_name('John') - - self.assertFalse(self.page.is_browser_on_page(), 'Should redirect to the admin user list view on success') - - # Visit the page again to verify changes - self.page.visit() - - self.assertEqual(self.page.first_name, 'John', 'The first name should be updated') diff --git a/common/test/acceptance/tests/studio/test_import_export.py b/common/test/acceptance/tests/studio/test_import_export.py deleted file mode 100644 index 12a0e699a5..0000000000 --- a/common/test/acceptance/tests/studio/test_import_export.py +++ /dev/null @@ -1,220 +0,0 @@ -""" -Acceptance tests for the Import and Export pages -""" - - -from abc import abstractmethod -from datetime import datetime - -from common.test.acceptance.pages.studio.import_export import ( - ExportCoursePage, - ExportLibraryPage, - ImportCoursePage, - ImportLibraryPage -) -from common.test.acceptance.pages.studio.library import LibraryEditPage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage -from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest, StudioLibraryTest -from openedx.core.lib.tests import attr - - -class ExportTestMixin(object): - """ - Tests to run both for course and library export pages. - """ - def test_export(self): - """ - Scenario: I am able to export a course or library - Given that I have a course or library - And I click the download button - The download will succeed - And the file will be of the right MIME type. - """ - self.export_page.wait_for_export_click_handler() - self.export_page.click_export() - self.export_page.wait_for_export() - good_status, is_tarball_mimetype = self.export_page.download_tarball() - self.assertTrue(good_status) - self.assertTrue(is_tarball_mimetype) - - def test_export_timestamp(self): - """ - Scenario: I perform a course / library export - On export success, the page displays a UTC timestamp previously not visible - And if I refresh the page, the timestamp is still displayed - """ - self.assertFalse(self.export_page.is_timestamp_visible()) - - # Get the time when the export has started. - # export_page timestamp is in (MM/DD/YYYY at HH:mm) so replacing (second, microsecond) to - # keep the comparison consistent - export_start_time = datetime.utcnow().replace(microsecond=0, second=0) - self.export_page.wait_for_export_click_handler() - self.export_page.click_export() - self.export_page.wait_for_export() - - # Get the time when the export has finished. - # export_page timestamp is in (MM/DD/YYYY at HH:mm) so replacing (second, microsecond) to - # keep the comparison consistent - export_finish_time = datetime.utcnow().replace(microsecond=0, second=0) - - export_timestamp = self.export_page.parsed_timestamp - self.export_page.wait_for_timestamp_visible() - - # Verify that 'export_timestamp' is between start and finish upload time - self.assertLessEqual( - export_start_time, - export_timestamp, - "Course export timestamp should be export_start_time <= export_timestamp <= export_end_time" - ) - self.assertGreaterEqual( - export_finish_time, - export_timestamp, - "Course export timestamp should be export_start_time <= export_timestamp <= export_end_time" - ) - - self.export_page.visit() - self.export_page.wait_for_tasks(completed=True) - self.export_page.wait_for_timestamp_visible() - - def test_task_list(self): - """ - Scenario: I should see feedback checkpoints when exporting a course or library - Given that I am on an export page - No task checkpoint list should be showing - When I export the course or library - Each task in the checklist should be marked confirmed - And the task list should be visible - """ - # The task list shouldn't be visible to start. - self.assertFalse(self.export_page.is_task_list_showing(), "Task list shown too early.") - self.export_page.wait_for_tasks() - self.export_page.wait_for_export_click_handler() - self.export_page.click_export() - self.export_page.wait_for_tasks(completed=True) - self.assertTrue(self.export_page.is_task_list_showing(), "Task list did not display.") - - -@attr(shard=7) -class TestCourseExport(ExportTestMixin, StudioCourseTest): - """ - Export tests for courses. - """ - def setUp(self): # pylint: disable=arguments-differ - super(TestCourseExport, self).setUp() - self.export_page = ExportCoursePage( - self.browser, - self.course_info['org'], self.course_info['number'], self.course_info['run'], - ) - self.export_page.visit() - - def test_header(self): - """ - Scenario: I should see the correct text when exporting a course. - Given that I have a course to export from - When I visit the export page - The correct header should be shown - """ - self.assertEqual(self.export_page.header_text, 'Course Export') - - -@attr(shard=7) -class TestLibraryExport(ExportTestMixin, StudioLibraryTest): - """ - Export tests for libraries. - """ - def setUp(self): - """ - Ensure a library exists and navigate to the library edit page. - """ - super(TestLibraryExport, self).setUp() - self.export_page = ExportLibraryPage(self.browser, self.library_key) - self.export_page.visit() - - def test_header(self): - """ - Scenario: I should see the correct text when exporting a library. - Given that I have a library to export from - When I visit the export page - The correct header should be shown - """ - self.assertEqual(self.export_page.header_text, 'Library Export') - - -@attr(shard=7) -class ImportTestMixin(object): - """ - Tests to run for both course and library import pages. - """ - def setUp(self): - super(ImportTestMixin, self).setUp() - self.import_page = self.import_page_class(*self.page_args()) - self.landing_page = self.landing_page_class(*self.page_args()) - self.import_page.visit() - - @abstractmethod - def page_args(self): - """ - Generates the args for initializing a page object. - """ - return [] - - -@attr(shard=7) -class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest): - """ - Tests the Course import page - """ - tarball_name = 'entrance_exam_course.2015.tar.gz' - bad_tarball_name = 'bad_course.tar.gz' - import_page_class = ImportCoursePage - landing_page_class = CourseOutlinePage - - def page_args(self): - return [self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']] - - -@attr(shard=7) -class TestCourseImport(ImportTestMixin, StudioCourseTest): - """ - Tests the Course import page - """ - tarball_name = '2015.lzdwNM.tar.gz' - bad_tarball_name = 'bad_course.tar.gz' - import_page_class = ImportCoursePage - landing_page_class = CourseOutlinePage - - def page_args(self): - return [self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']] - - def test_header(self): - """ - Scenario: I should see the correct text when importing a course. - Given that I have a course to import to - When I visit the import page - The correct header should be shown - """ - self.assertEqual(self.import_page.header_text, 'Course Import') - - -@attr(shard=7) -class TestLibraryImport(ImportTestMixin, StudioLibraryTest): - """ - Tests the Library import page - """ - tarball_name = 'library.HhJfPD.tar.gz' - bad_tarball_name = 'bad_library.tar.gz' - import_page_class = ImportLibraryPage - landing_page_class = LibraryEditPage - - def page_args(self): - return [self.browser, self.library_key] - - def test_header(self): - """ - Scenario: I should see the correct text when importing a library. - Given that I have a library to import to - When I visit the import page - The correct header should be shown - """ - self.assertEqual(self.import_page.header_text, 'Library Import') diff --git a/common/test/acceptance/tests/studio/test_studio_acid_xblock.py b/common/test/acceptance/tests/studio/test_studio_acid_xblock.py deleted file mode 100644 index 4f80858418..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_acid_xblock.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -Acceptance tests for Studio related to the acid xblock. -""" - - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage -from common.test.acceptance.pages.xblock.acid import AcidView -from common.test.acceptance.tests.helpers import AcceptanceTest - - -class XBlockAcidBase(AcceptanceTest): - """ - Base class for tests that verify that XBlock integration is working correctly - """ - shard = 21 - __test__ = False - - def setUp(self): - """ - Create a unique identifier for the course used in this test. - """ - # Ensure that the superclass sets up - super(XBlockAcidBase, self).setUp() - - # Define a unique course identifier - self.course_info = { - 'org': 'test_org', - 'number': 'course_' + self.unique_id[:5], - 'run': 'test_' + self.unique_id, - 'display_name': 'Test Course ' + self.unique_id - } - - self.outline = CourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self.course_id = '{org}.{number}.{run}'.format(**self.course_info) - - self.setup_fixtures() - - self.auth_page = AutoAuthPage( - self.browser, - staff=False, - username=self.user.get('username'), - email=self.user.get('email'), - password=self.user.get('password') - ) - self.auth_page.visit() - - def validate_acid_block_preview(self, acid_block): - """ - Validate the Acid Block's preview - """ - self.assertTrue(acid_block.init_fn_passed) - self.assertTrue(acid_block.resource_url_passed) - self.assertTrue(acid_block.scope_passed('user_state')) - self.assertTrue(acid_block.scope_passed('user_state_summary')) - self.assertTrue(acid_block.scope_passed('preferences')) - self.assertTrue(acid_block.scope_passed('user_info')) - - def test_acid_block_preview(self): - """ - Verify that all expected acid block tests pass in studio preview - """ - - self.outline.visit() - subsection = self.outline.section('Test Section').subsection('Test Subsection') - unit = subsection.expand_subsection().unit('Test Unit').go_to() - - acid_block = AcidView(self.browser, unit.xblocks[0].preview_selector) - self.validate_acid_block_preview(acid_block) - - def test_acid_block_editor(self): - """ - Verify that all expected acid block tests pass in studio editor - """ - - self.outline.visit() - subsection = self.outline.section('Test Section').subsection('Test Subsection') - unit = subsection.expand_subsection().unit('Test Unit').go_to() - - acid_block = AcidView(self.browser, unit.xblocks[0].edit().editor_selector) - self.assertTrue(acid_block.init_fn_passed) - self.assertTrue(acid_block.resource_url_passed) - - -class XBlockAcidNoChildTest(XBlockAcidBase): - """ - Tests of an AcidBlock with no children - """ - __test__ = True - - def setup_fixtures(self): - - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid', 'Acid Block') - ) - ) - ) - ).install() - - self.user = course_fix.user - - -class XBlockAcidParentBase(XBlockAcidBase): - """ - Base class for tests that verify that parent XBlock integration is working correctly - """ - __test__ = False - - def validate_acid_block_preview(self, acid_block): - super(XBlockAcidParentBase, self).validate_acid_block_preview(acid_block) - self.assertTrue(acid_block.child_tests_passed) - - def test_acid_block_preview(self): - """ - Verify that all expected acid block tests pass in studio preview - """ - - self.outline.visit() - subsection = self.outline.section('Test Section').subsection('Test Subsection') - unit = subsection.expand_subsection().unit('Test Unit').go_to() - container = unit.xblocks[0].go_to_container() - - acid_block = AcidView(self.browser, container.xblocks[0].preview_selector) - self.validate_acid_block_preview(acid_block) - - -class XBlockAcidEmptyParentTest(XBlockAcidParentBase): - """ - Tests of an AcidBlock with children - """ - __test__ = True - - def setup_fixtures(self): - - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid_parent', 'Acid Parent Block').add_children( - ) - ) - ) - ) - ).install() - - self.user = course_fix.user - - -class XBlockAcidChildTest(XBlockAcidParentBase): - """ - Tests of an AcidBlock with children - """ - __test__ = True - - def setup_fixtures(self): - - course_fix = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'] - ) - - course_fix.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('acid_parent', 'Acid Parent Block').add_children( - XBlockFixtureDesc('acid', 'First Acid Child', metadata={'name': 'first'}), - XBlockFixtureDesc('acid', 'Second Acid Child', metadata={'name': 'second'}), - XBlockFixtureDesc('html', 'Html Child', data="Contents"), - ) - ) - ) - ) - ).install() - - self.user = course_fix.user - - def test_acid_block_preview(self): - super(XBlockAcidChildTest, self).test_acid_block_preview() - - def test_acid_block_editor(self): - super(XBlockAcidChildTest, self).test_acid_block_editor() diff --git a/common/test/acceptance/tests/studio/test_studio_asset.py b/common/test/acceptance/tests/studio/test_studio_asset.py deleted file mode 100644 index 1f7282e808..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_asset.py +++ /dev/null @@ -1,250 +0,0 @@ -""" -Acceptance tests for Studio related to the asset index page. -""" - - -import os - -from common.test.acceptance.pages.studio.asset_index import UPLOAD_FILE_DIR, AssetIndexPageStudioFrontend -from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest - - -class AssetIndexTestStudioFrontend(StudioCourseTest): - """Tests for the Asset index page.""" - shard = 21 - - def setUp(self, is_staff=False): # pylint: disable=arguments-differ - super(AssetIndexTestStudioFrontend, self).setUp() - self.asset_page = AssetIndexPageStudioFrontend( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - def populate_course_fixture(self, course_fixture): - """Populate the children of the test course fixture.""" - self.course_fixture.add_asset(['image.jpg', 'textbook.pdf']) - - def test_page_with_assets_elements_load(self): - """Make sure all elements are on page for a course with assets.""" - self.asset_page.visit() - assert self.assert_studio_frontend_container_exists() - assert self.assert_table_exists() - assert self.assert_type_filter_exists() - assert self.assert_upload_element_exists() - assert self.assert_sortable_table_heading_elements_exist() - assert self.assert_status_element_exists() - assert self.assert_pagination_element_exists() - assert self.assert_search_element_exists() - - def assert_page_without_filter_results_elements_load(self): - """Make sure correct elements are on page for a filter with no results.""" - assert self.assert_studio_frontend_container_exists() - assert not self.assert_table_exists() - assert not self.assert_sortable_table_heading_elements_exist() - assert not self.assert_pagination_element_exists() - - assert self.assert_search_element_exists() - assert self.assert_status_element_exists() - assert self.assert_type_filter_exists() - assert self.assert_upload_element_exists() - assert self.assert_no_results_headings_exist() - assert self.assert_clear_filters_button_exists() - - def assert_studio_frontend_container_exists(self): - return self.asset_page.is_studio_frontend_container_on_page() - - def assert_table_exists(self): - """Make sure table is on the page.""" - return self.asset_page.is_table_element_on_page() - - def assert_type_filter_exists(self): - """Make sure type filter is on the page.""" - return self.asset_page.is_filter_element_on_page() is True - - def assert_upload_element_exists(self): - """Make sure upload dropzone is on the page.""" - return self.asset_page.is_upload_element_on_page() - - def assert_sortable_table_heading_elements_exist(self): - """ - Make sure the sortable table buttons are on the page and there arethree of them.""" - return self.asset_page.number_of_sortable_buttons_in_table_heading == 3 - - def assert_status_element_exists(self): - """Make sure status alert is on the page but not visible.""" - return self.asset_page.is_status_alert_element_on_page() - - def assert_pagination_element_exists(self): - """Make sure pagination element is on the page.""" - return self.asset_page.is_pagination_element_on_page() is True - - def assert_search_element_exists(self): - """Make sure search element is on the page.""" - return self.asset_page.is_search_element_on_page() is True - - def assert_no_results_headings_exist(self): - """Make sure headings with text for no results is on the page.""" - return self.asset_page.are_no_results_headings_on_page() - - def assert_clear_filters_button_exists(self): - """Make sure the clear filters button is on the page.""" - return self.asset_page.is_no_results_clear_filter_button_on_page() - - def test_clicking_filter_with_results(self): - """Make sure clicking the Images filter that has results and performs the filtering correctly.""" - self.asset_page.visit() - all_results = self.asset_page.number_of_asset_files - # select Images - assert self.asset_page.select_type_filter(3) - filtered_results = self.asset_page.number_of_asset_files - assert all_results > filtered_results - assets_file_types = self.asset_page.asset_files_types - for file_type in assets_file_types: - assert 'image' in file_type - - def test_clicking_filter_without_results(self): - """ - Make sure clicking a type filter that has no results performs the filtering correctly, updates the page view to - display the no results view, and displays the correct elements. - """ - self.asset_page.visit() - all_results = self.asset_page.number_of_asset_files - # select Audio - assert self.asset_page.select_type_filter(0) - filtered_results = self.asset_page.number_of_asset_files - assert all_results > filtered_results - assert filtered_results == 0 - self.assert_page_without_filter_results_elements_load() - - def test_clicking_clear_filter(self): - """Make sure clicking the 'Clear filter' button clears the checkbox and returns results.""" - self.asset_page.visit() - all_results = self.asset_page.number_of_asset_files - # select Audio - assert self.asset_page.select_type_filter(0) - assert self.asset_page.click_clear_filters_button() - new_results = self.asset_page.number_of_asset_files - assert new_results == all_results - self.test_page_with_assets_elements_load() - - def test_lock(self): - """Make sure clicking the lock button toggles correctly.""" - self.asset_page.visit() - # Verify that a file can be locked - self.asset_page.set_asset_lock() - # Get the list of locked assets, there should be one - locked_assets = self.asset_page.asset_lock_buttons(locked_only=True) - self.assertEqual(len(locked_assets), 1) - - # Confirm that there are 2 assets, with the first - # locked and the second unlocked. - all_assets = self.asset_page.asset_lock_buttons(locked_only=False) - self.assertEqual(len(all_assets), 2) - self.assertTrue('fa-lock' in all_assets[0].get_attribute('class')) - self.assertTrue('fa-unlock' in all_assets[1].get_attribute('class')) - - def test_delete_and_upload(self): - """ - Upload specific files to page. - Start by deleting all files, to ensure starting on a blank slate. - """ - self.asset_page.visit() - self.asset_page.delete_all_assets() - file_names = [u'file-0.png', u'file-13.pdf', u'file-26.js', u'file-39.txt'] - # Upload the files - self.asset_page.upload_new_files(file_names) - # Assert that the files have been uploaded. - all_assets = self.asset_page.number_of_asset_files - self.assertEqual(all_assets, 4) - self.assertEqual(file_names.sort(), self.asset_page.asset_files_names.sort()) - - def test_display_name_sort(self): - """Make sure clicking the display name sort button sorts the files.""" - self.asset_page.visit() - # the default sort is on 'Date Added', so sort on 'Name' to start - # with a fresh state - self.asset_page.click_sort_button('Name') - before_sort_file_names = self.asset_page.asset_files_names - sorted_file_names = sorted(before_sort_file_names) - - assert self.asset_page.click_sort_button('Name') - after_sort_file_names = self.asset_page.asset_files_names - assert before_sort_file_names != after_sort_file_names - assert sorted_file_names == after_sort_file_names - - assert self.asset_page.click_sort_button('Name') - assert self.asset_page.asset_files_names == before_sort_file_names - - -class AssetIndexTestStudioFrontendPagination(StudioCourseTest): - """Pagination tests for the Asset index page.""" - shard = 23 - - def setUp(self, is_staff=False): # pylint: disable=arguments-differ - super(AssetIndexTestStudioFrontendPagination, self).setUp() - self.asset_page = AssetIndexPageStudioFrontend( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - def populate_course_fixture(self, course_fixture): - """Populate the children of the test course fixture and upload 49 files.""" - files = [] - - for file_name in os.listdir(UPLOAD_FILE_DIR): - file_path = 'studio-uploads/' + file_name - files.append(file_path) - course_fixture.add_asset(files) - - def assert_correct_number_of_buttons(self, count): - """Make sure the correct number of buttons are on the page; includes previous and next. """ - assert self.asset_page.number_of_pagination_buttons == count - - def assert_correct_direction_buttons(self): - """Make sure the previous and next pagination buttons are on the page.""" - assert self.asset_page.is_previous_button_on_page - assert self.asset_page.is_next_button_on_page - - def test_pagination_exists(self): - """Make sure the pagination elements are on the page.""" - self.asset_page.visit() - self.assert_correct_number_of_buttons(4) - self.assert_correct_direction_buttons() - - def test_pagination_page_click(self): - """Make clicking the second page button displays the second page of files.""" - self.asset_page.visit() - - first_page_file_names = self.asset_page.asset_files_names - assert self.asset_page.click_pagination_page_button(2) - assert self.asset_page.is_selected_page(2) - assert self.asset_page.number_of_asset_files == 1 - second_page_file_names = self.asset_page.asset_files_names - - assert first_page_file_names != second_page_file_names - - def test_pagination_next_and_previous_click(self): - """ - Make sure clicking the next button displays the next page of files and - clicking the previous button displays the previous page of files. - """ - self.asset_page.visit() - - first_page_file_names = self.asset_page.asset_files_names - assert self.asset_page.click_pagination_next_button() - assert self.asset_page.is_selected_page(2) - assert self.asset_page.number_of_asset_files == 1 - next_page_file_names = self.asset_page.asset_files_names - - assert first_page_file_names != next_page_file_names - - assert self.asset_page.click_pagination_previous_button() - assert self.asset_page.is_selected_page(1) - assert self.asset_page.number_of_asset_files == 50 - previous_page_file_names = self.asset_page.asset_files_names - - assert first_page_file_names == previous_page_file_names diff --git a/common/test/acceptance/tests/studio/test_studio_bad_data.py b/common/test/acceptance/tests/studio/test_studio_bad_data.py deleted file mode 100644 index 396b105d71..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_bad_data.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Acceptance tests that ensure components with bad content do not break page. -""" - - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.studio.utils import verify_ordering - -from .base_studio_test import ContainerBase - - -class BadComponentTest(ContainerBase): - """ - Tests that components with bad content do not break the Unit page. - """ - shard = 21 - __test__ = False - - def get_bad_html_content(self): - """ - Return the "bad" HTML content that has been problematic for Studio. - """ - pass - - def populate_course_fixture(self, course_fixture): - """ - Sets up a course structure with a unit and a HTML component with bad data and a properly constructed problem. - """ - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('html', 'Unit HTML', data=self.get_bad_html_content()), - XBlockFixtureDesc('problem', 'Unit Problem', data='') - ) - ) - ) - ) - - def test_html_comp_visible(self): - """ - Tests that bad HTML data within an HTML component doesn't prevent Studio from - displaying the components on the unit page. - """ - unit = self.go_to_unit_page() - verify_ordering(self, unit, [{"": ["Unit HTML", "Unit Problem"]}]) - - -class CopiedFromLmsBadContentTest(BadComponentTest): - """ - Tests that components with HTML copied from the LMS (LmsRuntime) do not break the Unit page. - """ - __test__ = True - - def get_bad_html_content(self): - """ - Return the "bad" HTML content that has been problematic for Studio. - """ - return """ -
-

Copied from LMS HTML component

- """ - - -class CopiedFromStudioBadContentTest(BadComponentTest): - """ - Tests that components with HTML copied from the Studio (containing "ui-sortable" class) do not break the Unit page. - """ - __test__ = True - - def get_bad_html_content(self): - """ - Return the "bad" HTML content that has been problematic for Studio. - """ - return """ -
    -
  1. -
    -

    VOICE COMPARISON

    -

    You can access the experimental Voice Comparison tool at the link below.

    -
    -
  2. -
- """ - - -class JSErrorBadContentTest(BadComponentTest): - """ - Tests that components that throw JS errors do not break the Unit page. - """ - __test__ = True - - def get_bad_html_content(self): - """ - Return the "bad" HTML content that has been problematic for Studio. - """ - return "" diff --git a/common/test/acceptance/tests/studio/test_studio_components.py b/common/test/acceptance/tests/studio/test_studio_components.py deleted file mode 100644 index 581406e35d..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_components.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Acceptance tests for adding components in Studio. -""" - - -import ddt - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.studio.container import ContainerPage -from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettingsPage -from common.test.acceptance.pages.studio.utils import add_component, add_components -from common.test.acceptance.tests.studio.base_studio_test import ContainerBase - - -@ddt.ddt -class AdvancedProblemComponentTest(ContainerBase): - """ - Feature: CMS.Component Adding - As a course author, I want to be able to add a wide variety of components - """ - shard = 22 - - def setUp(self, is_staff=True): - """ - Create a course with a section, subsection, and unit to which to add the component. - """ - super(AdvancedProblemComponentTest, self).setUp(is_staff=is_staff) - - def populate_course_fixture(self, course_fixture): - course_fixture.add_advanced_settings( - {u"advanced_modules": {"value": ["split_test"]}} - ) - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ) - ) - - @ddt.data( - 'Blank Advanced Problem', - 'Circuit Schematic Builder', - 'Custom Python-Evaluated Input', - 'Drag and Drop', - 'Image Mapped Input', - 'Math Expression Input', - 'Problem with Adaptive Hint', - ) - def test_add_advanced_problem(self, component): - """ - Scenario Outline: I can add Advanced Problem components - Given I am in Studio editing a new unit - When I add a "" "Advanced Problem" component - Then I see a "" Problem component - - Examples: - | Component | - | Blank Advanced Problem | - | Circuit Schematic Builder | - | Custom Python-Evaluated Input | - | Drag and Drop | - | Image Mapped Input | - | Math Expression Input | - | Problem with Adaptive Hint | - """ - self.go_to_unit_page() - page = ContainerPage(self.browser, None) - add_component(page, 'problem', component, is_advanced_problem=True) - problem = page.xblocks[1] - self.assertEqual(problem.name, component) - - -class ComponentTest(ContainerBase): - """ - Test class to add different components. - (Not the advanced components) - """ - shard = 22 - - def setUp(self, is_staff=True): - """ - Create a course with a section, subsection, and unit to which to add the component. - """ - super(ComponentTest, self).setUp(is_staff=is_staff) - self.advanced_settings = AdvancedSettingsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - def populate_course_fixture(self, course_fixture): - course_fixture.add_advanced_settings( - {u"advanced_modules": {"value": ["split_test"]}} - ) - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ) - ) - - def test_add_html_component(self): - """ - Scenario: I can add HTML components - Given I am in Studio editing a new unit - When I add this type of HTML component: - | Component | - | Text | - | Announcement | - | Zooming Image Tool | - | Raw HTML | - Then I see HTML components in this order: - | Component | - | Text | - | Announcement | - | Zooming Image Tool | - | Raw HTML | - """ - # Components to be added - components = ['Text', 'Announcement', 'Zooming Image Tool', 'Raw HTML'] - self.go_to_unit_page() - container_page = ContainerPage(self.browser, None) - # Add components - add_components(container_page, 'html', components) - problems = [x_block.name for x_block in container_page.xblocks[1:]] - # Assert that components appear in same order as added. - self.assertEqual(problems, components) - - def test_add_latex_html_component(self): - """ - Scenario: I can add Latex HTML components - Given I am in Studio editing a new unit - Given I have enabled latex compiler - When I add this type of HTML component: - | Component | - | E-text Written in LaTeX | - Then I see HTML components in this order: - | Component | - | E-text Written in LaTeX | - """ - # Latex component - component = 'E-text Written in LaTeX' - # Visit advanced settings page and enable latex compiler. - self.advanced_settings.visit() - self.advanced_settings.set('Enable LaTeX Compiler', 'True') - self.go_to_unit_page() - container_page = ContainerPage(self.browser, None) - # Add latex component - add_component(container_page, 'html', component, is_advanced_problem=False) - problem = container_page.xblocks[1] - # Asset that component has been added. - self.assertEqual(problem.name, component) - - def test_common_problem_component(self): - """ - Scenario: I can add Common Problem components - Given I am in Studio editing a new unit - When I add this type of Problem component: - | Component |` - | Blank Common Problem | - | Checkboxes | - | Dropdown | - | Multiple Choice | - | Numerical Input | - | Text Input | - Then I see Problem components in this order: - | Component | - | Blank Common Problem | - | Checkboxes | - | Dropdown | - | Multiple Choice | - | Numerical Input | - | Text Input | - """ - # Components to be added. - components = ['Blank Common Problem', 'Checkboxes', 'Dropdown', - 'Multiple Choice', 'Numerical Input', 'Text Input'] - - self.go_to_unit_page() - container_page = ContainerPage(self.browser, None) - # Add components - add_components(container_page, 'problem', components) - problems = [x_block.name for x_block in container_page.xblocks[1:]] - # Assert that components appear in the same order as added. - self.assertEqual(problems, components) diff --git a/common/test/acceptance/tests/studio/test_studio_container.py b/common/test/acceptance/tests/studio/test_studio_container.py deleted file mode 100644 index bb4687a001..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_container.py +++ /dev/null @@ -1,1481 +0,0 @@ -""" -Acceptance tests for Studio related to the container page. -The container page is used both for displaying units, and -for displaying containers within units. -""" - - -import datetime - -import ddt -import six - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.create_mode import ModeCreationPage -from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage -from common.test.acceptance.pages.studio.container import ContainerPage -from common.test.acceptance.pages.studio.html_component_editor import HtmlXBlockEditorView -from common.test.acceptance.pages.studio.move_xblock import MoveModalView -from common.test.acceptance.pages.studio.utils import add_discussion -from common.test.acceptance.pages.studio.xblock_editor import XBlockEditorView, XBlockVisibilityEditorView -from common.test.acceptance.tests.helpers import create_user_partition_json -from openedx.core.lib.tests import attr -from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID, Group - -from .base_studio_test import ContainerBase - - -class NestedVerticalTest(ContainerBase): - - def populate_course_fixture(self, course_fixture): - """ - Sets up a course structure with nested verticals. - """ - self.container_title = "" - self.group_a = "Group A" - self.group_b = "Group B" - self.group_empty = "Group Empty" - self.group_a_item_1 = "Group A Item 1" - self.group_a_item_2 = "Group A Item 2" - self.group_b_item_1 = "Group B Item 1" - self.group_b_item_2 = "Group B Item 2" - - self.group_a_handle = 0 - self.group_a_item_1_handle = 1 - self.group_a_item_2_handle = 2 - self.group_empty_handle = 3 - self.group_b_handle = 4 - self.group_b_item_1_handle = 5 - self.group_b_item_2_handle = 6 - - self.group_a_item_1_action_index = 0 - self.group_a_item_2_action_index = 1 - - self.duplicate_label = u"Duplicate of '{0}'" - self.discussion_label = "Discussion" - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('vertical', 'Test Container').add_children( - XBlockFixtureDesc('vertical', 'Group A').add_children( - XBlockFixtureDesc('html', self.group_a_item_1), - XBlockFixtureDesc('html', self.group_a_item_2) - ), - XBlockFixtureDesc('vertical', 'Group Empty'), - XBlockFixtureDesc('vertical', 'Group B').add_children( - XBlockFixtureDesc('html', self.group_b_item_1), - XBlockFixtureDesc('html', self.group_b_item_2) - ) - ) - ) - ) - ) - ) - - -@attr(shard=1) -class AddComponentTest(NestedVerticalTest): - """ - Tests of adding a component to the container page. - """ - - def add_and_verify(self, menu_index, expected_ordering): - self.do_action_and_verify( - lambda container: add_discussion(container, menu_index), - expected_ordering - ) - - def test_add_component_in_group(self): - group_b_menu = 2 - - expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, - {self.group_a: [self.group_a_item_1, self.group_a_item_2]}, - {self.group_b: [self.group_b_item_1, self.group_b_item_2, self.discussion_label]}, - {self.group_empty: []}] - self.add_and_verify(group_b_menu, expected_ordering) - - def test_add_component_in_empty_group(self): - group_empty_menu = 1 - - expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, - {self.group_a: [self.group_a_item_1, self.group_a_item_2]}, - {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, - {self.group_empty: [self.discussion_label]}] - self.add_and_verify(group_empty_menu, expected_ordering) - - def test_add_component_in_container(self): - container_menu = 3 - - expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b, self.discussion_label]}, - {self.group_a: [self.group_a_item_1, self.group_a_item_2]}, - {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, - {self.group_empty: []}] - self.add_and_verify(container_menu, expected_ordering) - - -@attr(shard=1) -class DuplicateComponentTest(NestedVerticalTest): - """ - Tests of duplicating a component on the container page. - """ - - def duplicate_and_verify(self, source_index, expected_ordering): - self.do_action_and_verify( - lambda container: container.duplicate(source_index), - expected_ordering - ) - - def test_duplicate_first_in_group(self): - duplicate_label = self.duplicate_label.format(self.group_a_item_1) - expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, - {self.group_a: [self.group_a_item_1, duplicate_label, self.group_a_item_2]}, - {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, - {self.group_empty: []}] - self.duplicate_and_verify(self.group_a_item_1_action_index, expected_ordering) - - def test_duplicate_second_in_group(self): - duplicate_label = self.duplicate_label.format(self.group_a_item_2) - expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, - {self.group_a: [self.group_a_item_1, self.group_a_item_2, duplicate_label]}, - {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, - {self.group_empty: []}] - self.duplicate_and_verify(self.group_a_item_2_action_index, expected_ordering) - - def test_duplicate_the_duplicate(self): - first_duplicate_label = self.duplicate_label.format(self.group_a_item_1) - second_duplicate_label = self.duplicate_label.format(first_duplicate_label) - - expected_ordering = [ - {self.container_title: [self.group_a, self.group_empty, self.group_b]}, - {self.group_a: [self.group_a_item_1, first_duplicate_label, second_duplicate_label, self.group_a_item_2]}, - {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, - {self.group_empty: []} - ] - - def duplicate_twice(container): - container.duplicate(self.group_a_item_1_action_index) - container.duplicate(self.group_a_item_1_action_index + 1) - - self.do_action_and_verify(duplicate_twice, expected_ordering) - - -@attr(shard=1) -class DeleteComponentTest(NestedVerticalTest): - """ - Tests of deleting a component from the container page. - """ - - def delete_and_verify(self, source_index, expected_ordering): - self.do_action_and_verify( - lambda container: container.delete(source_index), - expected_ordering - ) - - def test_delete_first_in_group(self): - expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]}, - {self.group_a: [self.group_a_item_2]}, - {self.group_b: [self.group_b_item_1, self.group_b_item_2]}, - {self.group_empty: []}] - - # Group A itself has a delete icon now, so item_1 is index 1 instead of 0. - group_a_item_1_delete_index = 1 - self.delete_and_verify(group_a_item_1_delete_index, expected_ordering) - - -@attr(shard=19) -class EditContainerTest(NestedVerticalTest): - """ - Tests of editing a container. - """ - - def modify_display_name_and_verify(self, component): - """ - Helper method for changing a display name. - """ - modified_name = 'modified' - self.assertNotEqual(component.name, modified_name) - component.edit() - component_editor = XBlockEditorView(self.browser, component.locator) - component_editor.set_field_value_and_save('Display Name', modified_name) - self.assertEqual(component.name, modified_name) - - def test_edit_container_on_unit_page(self): - """ - Test the "edit" button on a container appearing on the unit page. - """ - unit = self.go_to_unit_page() - component = unit.xblocks[1] - self.modify_display_name_and_verify(component) - - def test_edit_container_on_container_page(self): - """ - Test the "edit" button on a container appearing on the container page. - """ - container = self.go_to_nested_container_page() - self.modify_display_name_and_verify(container) - - -class BaseGroupConfigurationsTest(ContainerBase): - ALL_LEARNERS_AND_STAFF = XBlockVisibilityEditorView.ALL_LEARNERS_AND_STAFF - CHOOSE_ONE = "Select a group type" - CONTENT_GROUP_PARTITION = XBlockVisibilityEditorView.CONTENT_GROUP_PARTITION - ENROLLMENT_TRACK_PARTITION = XBlockVisibilityEditorView.ENROLLMENT_TRACK_PARTITION - MISSING_GROUP_LABEL = 'Deleted Group\nThis group no longer exists. Choose another group or remove the access restriction.' - VALIDATION_ERROR_LABEL = 'This component has validation issues.' - VALIDATION_ERROR_MESSAGE = "Error:\nThis component's access settings refer to deleted or invalid groups." - GROUP_VISIBILITY_MESSAGE = 'Access to some content in this unit is restricted to specific groups of learners.' - MODAL_NOT_RESTRICTED_MESSAGE = "Access is not restricted" - - def setUp(self): - super(BaseGroupConfigurationsTest, self).setUp() - - # Set up a cohort-schemed user partition - self.id_base = MINIMUM_STATIC_PARTITION_ID - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - u"user_partitions": [ - create_user_partition_json( - self.id_base, - self.CONTENT_GROUP_PARTITION, - 'Content Group Partition', - [ - Group(self.id_base + 1, 'Dogs'), - Group(self.id_base + 2, 'Cats') - ], - scheme="cohort" - ) - ], - }, - }) - - self.container_page = self.go_to_unit_page() - self.html_component = self.container_page.xblocks[1] - - def populate_course_fixture(self, course_fixture): - """ - Populate a simple course a section, subsection, and unit, and HTML component. - """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('html', 'Html Component') - ) - ) - ) - ) - - def edit_component_visibility(self, component): - """ - Edit the visibility of an xblock on the container page and returns an XBlockVisibilityEditorView. - """ - component.edit_visibility() - return XBlockVisibilityEditorView(self.browser, component.locator) - - def edit_unit_visibility(self, unit): - """ - Edit the visibility of a unit on the container page and returns an XBlockVisibilityEditorView. - """ - unit.edit_visibility() - return XBlockVisibilityEditorView(self.browser, unit.locator) - - def verify_current_groups_message(self, visibility_editor, expected_current_groups): - """ - Check that the current visibility is displayed at the top of the dialog. - """ - if expected_current_groups == self.ALL_LEARNERS_AND_STAFF: - self.assertEqual("Access is not restricted", visibility_editor.current_groups_message) - else: - self.assertEqual( - u"Access is restricted to: {groups}".format(groups=expected_current_groups), - visibility_editor.current_groups_message - ) - - def verify_selected_partition_scheme(self, visibility_editor, expected_scheme): - """ - Check that the expected partition scheme is selected. - """ - six.assertCountEqual(self, expected_scheme, visibility_editor.selected_partition_scheme) - - def verify_selected_groups(self, visibility_editor, expected_groups): - """ - Check the expected partition groups. - """ - six.assertCountEqual(self, expected_groups, [group.text for group in visibility_editor.selected_groups]) - - def select_and_verify_saved(self, component, partition_label, groups=[]): - """ - Edit the visibility of an xblock on the container page and - verify that the edit persists. Note that `groups` - are labels which should be clicked, but not necessarily checked. - """ - # Make initial edit(s) and save - visibility_editor = self.edit_component_visibility(component) - - visibility_editor.select_groups_in_partition_scheme(partition_label, groups) - - # Re-open the modal and inspect its selected inputs. If no groups were selected, - # "All Learners" should be selected partitions scheme, and we show "Select a group type" in the select. - if not groups: - partition_label = self.CHOOSE_ONE - visibility_editor = self.edit_component_visibility(component) - self.verify_selected_partition_scheme(visibility_editor, partition_label) - self.verify_selected_groups(visibility_editor, groups) - visibility_editor.save() - - def select_and_verify_unit_group_access(self, unit, partition_label, groups=[]): - """ - Edit the visibility of an xblock on the unit page and - verify that the edit persists. Note that `groups` - are labels which should be clicked, but are not necessarily checked. - """ - unit_access_editor = self.edit_unit_visibility(unit) - unit_access_editor.select_groups_in_partition_scheme(partition_label, groups) - - if not groups: - partition_label = self.CHOOSE_ONE - unit_access_editor = self.edit_unit_visibility(unit) - self.verify_selected_partition_scheme(unit_access_editor, partition_label) - self.verify_selected_groups(unit_access_editor, groups) - unit_access_editor.save() - - def verify_component_validation_error(self, component): - """ - Verify that we see validation errors for the given component. - """ - self.assertTrue(component.has_validation_error) - self.assertEqual(component.validation_error_text, self.VALIDATION_ERROR_LABEL) - self.assertEqual([self.VALIDATION_ERROR_MESSAGE], component.validation_error_messages) - - def verify_visibility_set(self, component, is_set): - """ - Verify that the container page shows that component visibility - settings have been edited if `is_set` is True; otherwise - verify that the container page shows no such information. - """ - if is_set: - self.assertIn(self.GROUP_VISIBILITY_MESSAGE, self.container_page.sidebar_visibility_message) - self.assertTrue(component.has_group_visibility_set) - else: - self.assertNotIn(self.GROUP_VISIBILITY_MESSAGE, self.container_page.sidebar_visibility_message) - self.assertFalse(component.has_group_visibility_set) - - def verify_unit_visibility_set(self, unit, set_groups=[]): - """ - Verify that the container visibility modal shows that unit visibility - settings have been edited if there are `set_groups`. Otherwise verify - that the modal shows no such information. - """ - unit_access_editor = self.edit_unit_visibility(unit) - if set_groups: - self.assertIn(", ".join(set_groups), unit_access_editor.current_groups_message) - else: - self.assertEqual(self.MODAL_NOT_RESTRICTED_MESSAGE, unit_access_editor.current_groups_message) - unit_access_editor.cancel() - - def update_component(self, component, metadata): - """ - Update a component's metadata and refresh the page. - """ - self.course_fixture._update_xblock(component.locator, {'metadata': metadata}) - self.browser.refresh() - self.container_page.wait_for_page() - - def remove_missing_groups(self, visibility_editor, component): - """ - Deselect the missing groups for a component. After save, - verify that there are no missing group messages in the modal - and that there is no validation error on the component. - """ - for option in visibility_editor.all_group_options: - if option.text == self.MISSING_GROUP_LABEL: - option.click() - visibility_editor.save() - visibility_editor = self.edit_component_visibility(component) - self.assertNotIn(self.MISSING_GROUP_LABEL, [item.text for item in visibility_editor.all_group_options]) - visibility_editor.cancel() - self.assertFalse(component.has_validation_error) - - -@attr(shard=21) -class UnitAccessContainerTest(BaseGroupConfigurationsTest): - """ - Tests unit level access - """ - GROUP_RESTRICTED_MESSAGE = 'Access to this unit is restricted to: Dogs' - - def _toggle_container_unit_access(self, group_ids, unit): - """ - Toggle the unit level access on the course outline page - """ - unit.toggle_unit_access('Content Groups', group_ids) - - def _verify_container_unit_access_message(self, group_ids, expected_message): - """ - Check that the container page displays the correct unit - access message. - """ - self.outline.visit() - self.outline.expand_all_subsections() - unit = self.outline.section_at(0).subsection_at(0).unit_at(0) - self._toggle_container_unit_access(group_ids, unit) - - container_page = self.go_to_unit_page() - self.assertEqual(str(container_page.get_xblock_access_message()), expected_message) - - def test_default_selection(self): - """ - Tests that no message is displayed when there are no - restrictions on the unit or components. - """ - self._verify_container_unit_access_message([], '') - - def test_restricted_components_message(self): - """ - Test that the proper message is displayed when access to - some components is restricted. - """ - container_page = self.go_to_unit_page() - html_component = container_page.xblocks[1] - - # Initially set visibility to Dog group. - self.update_component( - html_component, - {'group_access': {self.id_base: [self.id_base + 1]}} - ) - - self._verify_container_unit_access_message([], self.GROUP_VISIBILITY_MESSAGE) - - def test_restricted_access_message(self): - """ - Test that the proper message is displayed when access to the - unit is restricted to a particular group. - """ - self._verify_container_unit_access_message([self.id_base + 1], self.GROUP_RESTRICTED_MESSAGE) - - -@attr(shard=9) -class ContentGroupVisibilityModalTest(BaseGroupConfigurationsTest): - """ - Tests of the visibility settings modal for components on the unit - page (content groups). - """ - def test_default_selection(self): - """ - Scenario: The component visibility modal selects visible to all by default. - Given I have a unit with one component - When I go to the container page for that unit - And I open the visibility editor modal for that unit's component - Then the default visibility selection should be 'All Students and Staff' - And the container page should not display the content visibility warning - """ - visibility_dialog = self.edit_component_visibility(self.html_component) - self.verify_current_groups_message(visibility_dialog, self.ALL_LEARNERS_AND_STAFF) - self.verify_selected_partition_scheme(visibility_dialog, self.CHOOSE_ONE) - visibility_dialog.cancel() - self.verify_visibility_set(self.html_component, False) - - def test_reset_to_all_students_and_staff(self): - """ - Scenario: The component visibility modal can be set to be visible to all students and staff. - Given I have a unit with one component - When I go to the container page for that unit - Then the container page should not display the content visibility warning by default. - If I then restrict access and save, and then I open the visibility editor modal for that unit's component - And I select 'All Students and Staff' - And I save the modal - Then the visibility selection should be 'All Students and Staff' - And the container page should still not display the content visibility warning - """ - self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs']) - self.select_and_verify_saved(self.html_component, self.ALL_LEARNERS_AND_STAFF) - self.verify_visibility_set(self.html_component, False) - - def test_reset_unit_access_to_all_students_and_staff(self): - """ - Scenario: The unit visibility modal can be set to be visible to all students and staff. - Given I have a unit - When I go to the container page for that unit - And I open the visibility editor modal for that unit - And I select 'Dogs' - And I save the modal - Then I re-open the modal, the unit access modal should display the content visibility settings - Then after re-opening the modal again - And I select 'All Learners and Staff' - And I save the modal - And I re-open the modal, the unit access modal should display that no content is restricted - """ - self.select_and_verify_unit_group_access(self.container_page, self.CONTENT_GROUP_PARTITION, ['Dogs']) - self.verify_unit_visibility_set(self.container_page, set_groups=["Dogs"]) - self.select_and_verify_unit_group_access(self.container_page, self.ALL_LEARNERS_AND_STAFF) - self.verify_unit_visibility_set(self.container_page) - - def test_select_single_content_group(self): - """ - Scenario: The component visibility modal can be set to be visible to one content group. - Given I have a unit with one component - When I go to the container page for that unit - And I open the visibility editor modal for that unit's component - And I select 'Dogs' - And I save the modal - Then the visibility selection should be 'Dogs' and 'Specific Content Groups' - """ - self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs']) - - def test_select_multiple_content_groups(self): - """ - Scenario: The component visibility modal can be set to be visible to multiple content groups. - Given I have a unit with one component - When I go to the container page for that unit - And I open the visibility editor modal for that unit's component - And I select 'Dogs' and 'Cats' - And I save the modal - Then the visibility selection should be 'Dogs', 'Cats', and 'Specific Content Groups' - """ - self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs', 'Cats']) - - def test_missing_groups(self): - """ - Scenario: The component visibility modal shows a validation error when visibility is set to multiple unknown - group ids. - Given I have a unit with one component - And that component's group access specifies multiple invalid group ids - When I go to the container page for that unit - Then I should see a validation error message on that unit's component - And I open the visibility editor modal for that unit's component - Then I should see that I have selected multiple deleted groups - And the container page should display the content visibility warning - And I de-select the missing groups - And I save the modal - Then the visibility selection should be 'All Students and Staff' - And I should not see any validation errors on the component - And the container page should not display the content visibility warning - """ - self.update_component( - self.html_component, - {'group_access': {self.id_base: [self.id_base + 3, self.id_base + 4]}} - ) - self._verify_and_remove_missing_content_groups( - "Deleted Group, Deleted Group", - [self.MISSING_GROUP_LABEL] * 2 - ) - self.verify_visibility_set(self.html_component, False) - - def test_found_and_missing_groups(self): - """ - Scenario: The component visibility modal shows a validation error when visibility is set to multiple unknown - group ids and multiple known group ids. - Given I have a unit with one component - And that component's group access specifies multiple invalid and valid group ids - When I go to the container page for that unit - Then I should see a validation error message on that unit's component - And I open the visibility editor modal for that unit's component - Then I should see that I have selected multiple deleted groups - And then if I de-select the missing groups - And I save the modal - Then the visibility selection should be the names of the valid groups. - And I should not see any validation errors on the component - """ - self.update_component( - self.html_component, - {'group_access': {self.id_base: [self.id_base + 1, self.id_base + 2, self.id_base + 3, self.id_base + 4]}} - ) - - self._verify_and_remove_missing_content_groups( - 'Dogs, Cats, Deleted Group, Deleted Group', - ['Dogs', 'Cats'] + [self.MISSING_GROUP_LABEL] * 2 - ) - - visibility_editor = self.edit_component_visibility(self.html_component) - self.verify_selected_partition_scheme(visibility_editor, self.CONTENT_GROUP_PARTITION) - expected_groups = ['Dogs', 'Cats'] - self.verify_current_groups_message(visibility_editor, ", ".join(expected_groups)) - self.verify_selected_groups(visibility_editor, expected_groups) - - def _verify_and_remove_missing_content_groups(self, current_groups_message, all_group_labels): - self.verify_component_validation_error(self.html_component) - visibility_editor = self.edit_component_visibility(self.html_component) - self.verify_selected_partition_scheme(visibility_editor, self.CONTENT_GROUP_PARTITION) - self.verify_current_groups_message(visibility_editor, current_groups_message) - self.verify_selected_groups(visibility_editor, all_group_labels) - self.remove_missing_groups(visibility_editor, self.html_component) - - -@attr(shard=20) -class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest): - """ - Tests of the visibility settings modal for components on the unit - page (enrollment tracks). - """ - AUDIT_TRACK = "Audit Track" - VERIFIED_TRACK = "Verified Track" - - def setUp(self): - super(EnrollmentTrackVisibilityModalTest, self).setUp() - - # Add an audit mode to the course - ModeCreationPage(self.browser, self.course_id, mode_slug=u'audit', mode_display_name=self.AUDIT_TRACK).visit() - - # Add a verified mode to the course - ModeCreationPage( - self.browser, self.course_id, mode_slug=u'verified', - mode_display_name=self.VERIFIED_TRACK, min_price=10 - ).visit() - - self.container_page = self.go_to_unit_page() - self.html_component = self.container_page.xblocks[1] - - # Initially set visibility to Verified track. - self.update_component( - self.html_component, - {'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [2]}} # "2" is Verified - ) - - def verify_component_group_visibility_messsage(self, component, expected_groups): - """ - Verifies that the group visibility message below the component display name is correct. - """ - if not expected_groups: - self.assertIsNone(component.get_partition_group_message) - else: - self.assertEqual("Access restricted to: " + expected_groups, component.get_partition_group_message) - - def test_setting_enrollment_tracks(self): - """ - Test that enrollment track groups can be selected. - """ - # Verify that the "Verified" Group is shown on the unit page (under the unit display name). - self.verify_component_group_visibility_messsage(self.html_component, "Verified Track") - - # Open dialog with "Verified" already selected. - visibility_editor = self.edit_component_visibility(self.html_component) - self.verify_current_groups_message(visibility_editor, self.VERIFIED_TRACK) - self.verify_selected_partition_scheme( - visibility_editor, - self.ENROLLMENT_TRACK_PARTITION - ) - self.verify_selected_groups(visibility_editor, [self.VERIFIED_TRACK]) - visibility_editor.cancel() - - # Select "All Learners and Staff". The helper method saves the change, - # then reopens the dialog to verify that it was persisted. - self.select_and_verify_saved(self.html_component, self.ALL_LEARNERS_AND_STAFF) - self.verify_component_group_visibility_messsage(self.html_component, None) - - # Select "Audit" enrollment track. The helper method saves the change, - # then reopens the dialog to verify that it was persisted. - self.select_and_verify_saved(self.html_component, self.ENROLLMENT_TRACK_PARTITION, [self.AUDIT_TRACK]) - self.verify_component_group_visibility_messsage(self.html_component, "Audit Track") - - -@attr(shard=16) -class UnitPublishingTest(ContainerBase): - """ - Tests of the publishing control and related widgets on the Unit page. - """ - - PUBLISHED_STATUS = "Publishing Status\nPublished (not yet released)" - PUBLISHED_LIVE_STATUS = "Publishing Status\nPublished and Live" - DRAFT_STATUS = "Publishing Status\nDraft (Unpublished changes)" - LOCKED_STATUS = "Publishing Status\nVisible to Staff Only" - RELEASE_TITLE_RELEASED = "RELEASED:" - RELEASE_TITLE_RELEASE = "RELEASE:" - - LAST_PUBLISHED = 'Last published' - LAST_SAVED = 'Draft saved on' - - def populate_course_fixture(self, course_fixture): - """ - Sets up a course structure with a unit and a single HTML child. - """ - - self.html_content = '

Body of HTML Unit.

' - self.courseware = CoursewarePage(self.browser, self.course_id) - past_start_date = datetime.datetime(1974, 6, 22) - self.past_start_date_text = "Jun 22, 1974 at 00:00 UTC" - future_start_date = datetime.datetime(2100, 9, 13) - - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('html', 'Test html', data=self.html_content) - ) - ) - ), - XBlockFixtureDesc( - 'chapter', - 'Unlocked Section', - metadata={'start': past_start_date.isoformat()} - ).add_children( - XBlockFixtureDesc('sequential', 'Unlocked Subsection').add_children( - XBlockFixtureDesc('vertical', 'Unlocked Unit').add_children( - XBlockFixtureDesc('problem', '', data=self.html_content) - ) - ) - ), - XBlockFixtureDesc('chapter', 'Section With Locked Unit').add_children( - XBlockFixtureDesc( - 'sequential', - 'Subsection With Locked Unit', - metadata={'start': past_start_date.isoformat()} - ).add_children( - XBlockFixtureDesc( - 'vertical', - 'Locked Unit', - metadata={'visible_to_staff_only': True} - ).add_children( - XBlockFixtureDesc('discussion', '', data=self.html_content) - ) - ) - ), - XBlockFixtureDesc( - 'chapter', - 'Unreleased Section', - metadata={'start': future_start_date.isoformat()} - ).add_children( - XBlockFixtureDesc('sequential', 'Unreleased Subsection').add_children( - XBlockFixtureDesc('vertical', 'Unreleased Unit') - ) - ) - ) - - def test_publishing(self): - """ - Scenario: The publish title changes based on whether or not draft content exists - Given I have a published unit with no unpublished changes - When I go to the unit page in Studio - Then the title in the Publish information box is "Published and Live" - And the Publish button is disabled - And the last published text contains "Last published" - And the last saved text contains "Last published" - And when I add a component to the unit - Then the title in the Publish information box is "Draft (Unpublished changes)" - And the last saved text contains "Draft saved on" - And the Publish button is enabled - And when I click the Publish button - Then the title in the Publish information box is "Published and Live" - And the last published text contains "Last published" - And the last saved text contains "Last published" - """ - unit = self.go_to_unit_page() - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - # Start date set in course fixture to 1970. - self._verify_release_date_info( - unit, self.RELEASE_TITLE_RELEASED, 'Jan 01, 1970 at 00:00 UTC\nwith Section "Test Section"' - ) - self._verify_last_published_and_saved(unit, self.LAST_PUBLISHED, self.LAST_PUBLISHED) - # Should not be able to click on Publish action -- but I don't know how to test that it is not clickable. - # TODO: continue discussion with Muhammad and Jay about this. - - # Add a component to the page so it will have unpublished changes. - add_discussion(unit) - unit.verify_publish_title(self.DRAFT_STATUS) - self._verify_last_published_and_saved(unit, self.LAST_PUBLISHED, self.LAST_SAVED) - unit.publish() - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - self._verify_last_published_and_saved(unit, self.LAST_PUBLISHED, self.LAST_PUBLISHED) - - def test_discard_changes(self): - """ - Scenario: The publish title changes after "Discard Changes" is clicked - Given I have a published unit with no unpublished changes - When I go to the unit page in Studio - Then the Discard Changes button is disabled - And I add a component to the unit - Then the title in the Publish information box is "Draft (Unpublished changes)" - And the Discard Changes button is enabled - And when I click the Discard Changes button - Then the title in the Publish information box is "Published and Live" - """ - unit = self.go_to_unit_page() - add_discussion(unit) - unit.verify_publish_title(self.DRAFT_STATUS) - unit.discard_changes() - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - - def test_view_live_no_changes(self): - """ - Scenario: "View Live" shows published content in LMS - Given I have a published unit with no unpublished changes - When I go to the unit page in Studio - Then the View Live button is enabled - And when I click on the View Live button - Then I see the published content in LMS - """ - unit = self.go_to_unit_page() - self._view_published_version(unit) - self._verify_components_visible(['html']) - - def test_view_live_changes(self): - """ - Scenario: "View Live" does not show draft content in LMS - Given I have a published unit with no unpublished changes - When I go to the unit page in Studio - And when I add a component to the unit - And when I click on the View Live button - Then I see the published content in LMS - And I do not see the unpublished component - """ - unit = self.go_to_unit_page() - add_discussion(unit) - self._view_published_version(unit) - self._verify_components_visible(['html']) - self.assertEqual(self.html_content, self.courseware.xblock_component_html_content(0)) - - def test_view_live_after_publish(self): - """ - Scenario: "View Live" shows newly published content - Given I have a published unit with no unpublished changes - When I go to the unit page in Studio - And when I add a component to the unit - And when I click the Publish button - And when I click on the View Live button - Then I see the newly published component - """ - unit = self.go_to_unit_page() - add_discussion(unit) - unit.publish() - self._view_published_version(unit) - self._verify_components_visible(['html', 'discussion']) - - def test_initially_unlocked_visible_to_students(self): - """ - Scenario: An unlocked unit with release date in the past is visible to students - Given I have a published unlocked unit with release date in the past - When I go to the unit page in Studio - Then the unit has a warning that it is visible to students - And it is marked as "RELEASED" with release date in the past visible - And when I click on the View Live Button - And when I view the course as a student - Then I see the content in the unit - """ - unit = self.go_to_unit_page("Unlocked Section", "Unlocked Subsection", "Unlocked Unit") - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - self.assertTrue(unit.currently_visible_to_students) - self._verify_release_date_info( - unit, self.RELEASE_TITLE_RELEASED, self.past_start_date_text + '\n' + 'with Section "Unlocked Section"' - ) - self._view_published_version(unit) - self._verify_student_view_visible(['problem']) - - def test_locked_visible_to_staff_only(self): - """ - Scenario: After locking a unit with release date in the past, it is only visible to staff - Given I have a published unlocked unit with release date in the past - When I go to the unit page in Studio - And when I select "Hide from students" - Then the unit does not have a warning that it is visible to students - And the unit does not display inherited staff lock - And when I click on the View Live Button - Then I see the content in the unit when logged in as staff - And when I view the course as a student - Then I do not see any content in the unit - """ - unit = self.go_to_unit_page("Unlocked Section", "Unlocked Subsection", "Unlocked Unit") - checked = unit.toggle_staff_lock() - self.assertTrue(checked) - self.assertFalse(unit.currently_visible_to_students) - self.assertFalse(unit.shows_inherited_staff_lock()) - unit.verify_publish_title(self.LOCKED_STATUS) - self._view_published_version(unit) - # Will initially be in staff view, locked component should be visible. - self._verify_components_visible(['problem']) - # Switch to student view and verify not visible - self._verify_student_view_locked() - - def test_initially_locked_not_visible_to_students(self): - """ - Scenario: A locked unit with release date in the past is not visible to students - Given I have a published locked unit with release date in the past - When I go to the unit page in Studio - Then the unit does not have a warning that it is visible to students - And it is marked as "RELEASE" with release date in the past visible - And when I click on the View Live Button - And when I view the course as a student - Then I do not see any content in the unit - """ - unit = self.go_to_unit_page("Section With Locked Unit", "Subsection With Locked Unit", "Locked Unit") - unit.verify_publish_title(self.LOCKED_STATUS) - self.assertFalse(unit.currently_visible_to_students) - self._verify_release_date_info( - unit, self.RELEASE_TITLE_RELEASE, - self.past_start_date_text + '\n' + 'with Subsection "Subsection With Locked Unit"' - ) - self._view_published_version(unit) - self._verify_student_view_locked() - - def test_unlocked_visible_to_all(self): - """ - Scenario: After unlocking a unit with release date in the past, it is visible to both students and staff - Given I have a published unlocked unit with release date in the past - When I go to the unit page in Studio - And when I deselect "Hide from students" - Then the unit does have a warning that it is visible to students - And when I click on the View Live Button - Then I see the content in the unit when logged in as staff - And when I view the course as a student - Then I see the content in the unit - """ - unit = self.go_to_unit_page("Section With Locked Unit", "Subsection With Locked Unit", "Locked Unit") - checked = unit.toggle_staff_lock() - self.assertFalse(checked) - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - self.assertTrue(unit.currently_visible_to_students) - self._view_published_version(unit) - # Will initially be in staff view, components always visible. - self._verify_components_visible(['discussion']) - # Switch to student view and verify visible. - self._verify_student_view_visible(['discussion']) - - def test_explicit_lock_overrides_implicit_subsection_lock_information(self): - """ - Scenario: A unit's explicit staff lock hides its inherited subsection staff lock information - Given I have a course with sections, subsections, and units - And I have enabled explicit staff lock on a subsection - When I visit the unit page - Then the unit page shows its inherited staff lock - And I enable explicit staff locking - Then the unit page does not show its inherited staff lock - And when I disable explicit staff locking - Then the unit page now shows its inherited staff lock - """ - self.outline.visit() - self.outline.expand_all_subsections() - subsection = self.outline.section_at(0).subsection_at(0) - unit = subsection.unit_at(0) - subsection.set_staff_lock(True) - unit_page = unit.go_to() - self._verify_explicit_lock_overrides_implicit_lock_information(unit_page) - - def test_explicit_lock_overrides_implicit_section_lock_information(self): - """ - Scenario: A unit's explicit staff lock hides its inherited subsection staff lock information - Given I have a course with sections, subsections, and units - And I have enabled explicit staff lock on a section - When I visit the unit page - Then the unit page shows its inherited staff lock - And I enable explicit staff locking - Then the unit page does not show its inherited staff lock - And when I disable explicit staff locking - Then the unit page now shows its inherited staff lock - """ - self.outline.visit() - self.outline.expand_all_subsections() - section = self.outline.section_at(0) - unit = section.subsection_at(0).unit_at(0) - section.set_staff_lock(True) - unit_page = unit.go_to() - self._verify_explicit_lock_overrides_implicit_lock_information(unit_page) - - def test_cancel_does_not_create_draft(self): - """ - Scenario: Editing a component and then canceling does not create a draft version (TNL-399) - Given I have a published unit with no unpublished changes - When I go to the unit page in Studio - And edit the content of an HTML component and then press cancel - Then the content does not change - And the title in the Publish information box is "Published and Live" - And when I reload the page - Then the title in the Publish information box is "Published and Live" - """ - unit = self.go_to_unit_page() - component = unit.xblocks[1] - component.edit() - HtmlXBlockEditorView(self.browser, component.locator).set_content_and_cancel("modified content") - self.assertEqual(component.student_content, "Body of HTML Unit.") - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - self.browser.refresh() - unit.wait_for_page() - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - - def test_delete_child_in_published_unit(self): - """ - Scenario: A published unit can be published again after deleting a child - Given I have a published unit with no unpublished changes - When I go to the unit page in Studio - And delete the only component - Then the title in the Publish information box is "Draft (Unpublished changes)" - And when I click the Publish button - Then the title in the Publish information box is "Published and Live" - And when I click the View Live button - Then I see an empty unit in LMS - """ - unit = self.go_to_unit_page() - unit.delete(0) - unit.verify_publish_title(self.DRAFT_STATUS) - unit.publish() - unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - self._view_published_version(unit) - self.assertEqual(0, self.courseware.num_xblock_components) - - def test_published_not_live(self): - """ - Scenario: The publish title displays correctly for units that are not live - Given I have a published unit with no unpublished changes that releases in the future - When I go to the unit page in Studio - Then the title in the Publish information box is "Published (not yet released)" - And when I add a component to the unit - Then the title in the Publish information box is "Draft (Unpublished changes)" - And when I click the Publish button - Then the title in the Publish information box is "Published (not yet released)" - """ - unit = self.go_to_unit_page('Unreleased Section', 'Unreleased Subsection', 'Unreleased Unit') - unit.verify_publish_title(self.PUBLISHED_STATUS) - add_discussion(unit) - unit.verify_publish_title(self.DRAFT_STATUS) - unit.publish() - unit.verify_publish_title(self.PUBLISHED_STATUS) - - def _view_published_version(self, unit): - """ - Goes to the published version, then waits for the browser to load the page. - """ - unit.view_published_version() - self.assertEqual(len(self.browser.window_handles), 2) - self.courseware.wait_for_page() - - def _verify_and_return_staff_page(self): - """ - Verifies that the browser is on the staff page and returns a StaffCoursewarePage. - """ - page = StaffCoursewarePage(self.browser, self.course_id) - page.wait_for_page() - return page - - def _verify_student_view_locked(self): - """ - Verifies no component is visible when viewing as a student. - """ - page = self._verify_and_return_staff_page() - page.set_staff_view_mode('Learner') - page.wait_for(lambda: self.courseware.num_xblock_components == 0, 'No XBlocks visible') - - def _verify_student_view_visible(self, expected_components): - """ - Verifies expected components are visible when viewing as a student. - """ - self._verify_and_return_staff_page().set_staff_view_mode('Learner') - self._verify_components_visible(expected_components) - - def _verify_components_visible(self, expected_components): - """ - Verifies the expected components are visible (and there are no extras). - """ - self.assertEqual(len(expected_components), self.courseware.num_xblock_components) - for index, component in enumerate(expected_components): - self.assertEqual(component, self.courseware.xblock_component_type(index)) - - def _verify_release_date_info(self, unit, expected_title, expected_date): - """ - Verifies how the release date is displayed in the publishing sidebar. - """ - self.assertEqual(expected_title, unit.release_title) - self.assertEqual(expected_date, unit.release_date) - - def _verify_last_published_and_saved(self, unit, expected_published_prefix, expected_saved_prefix): - """ - Verifies that last published and last saved messages respectively contain the given strings. - """ - self.assertIn(expected_published_prefix, unit.last_published_text) - self.assertIn(expected_saved_prefix, unit.last_saved_text) - - def _verify_explicit_lock_overrides_implicit_lock_information(self, unit_page): - """ - Verifies that a unit with inherited staff lock does not display inherited information when explicitly locked. - """ - self.assertTrue(unit_page.shows_inherited_staff_lock()) - unit_page.toggle_staff_lock(inherits_staff_lock=True) - self.assertFalse(unit_page.shows_inherited_staff_lock()) - unit_page.toggle_staff_lock(inherits_staff_lock=True) - self.assertTrue(unit_page.shows_inherited_staff_lock()) - - # TODO: need to work with Jay/Christine to get testing of "Preview" working. - # def test_preview(self): - # unit = self.go_to_unit_page() - # add_discussion(unit) - # unit.preview() - # self.assertEqual(2, self.courseware.num_xblock_components) - # self.assertEqual('html', self.courseware.xblock_component_type(0)) - # self.assertEqual('discussion', self.courseware.xblock_component_type(1)) - - -@attr(shard=20) -class DisplayNameTest(ContainerBase): - """ - Test consistent use of display_name_with_default - """ - def populate_course_fixture(self, course_fixture): - """ - Sets up a course structure with nested verticals. - """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('vertical', None) - ) - ) - ) - ) - - def test_display_name_default(self): - """ - Scenario: Given that an XBlock with a dynamic display name has been added to the course, - When I view the unit page and note the display name of the block, - Then I see the dynamically generated display name, - And when I then go to the container page for that same block, - Then I see the same generated display name. - """ - # Unfortunately no blocks in the core platform implement display_name_with_default - # in an interesting way for this test, so we are just testing for consistency and not - # the actual value. - unit = self.go_to_unit_page() - test_block = unit.xblocks[1] - title_on_unit_page = test_block.name - container = test_block.go_to_container() - self.assertEqual(container.name, title_on_unit_page) - - -@attr(shard=3) -class ProblemCategoryTabsTest(ContainerBase): - """ - Test to verify tabs in problem category. - """ - def setUp(self, is_staff=True): - super(ProblemCategoryTabsTest, self).setUp(is_staff=is_staff) - - def populate_course_fixture(self, course_fixture): - """ - Sets up course structure. - """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ) - ) - - def test_correct_tabs_present(self): - """ - Scenario: Verify that correct tabs are present in problem category. - - Given I am a staff user - When I go to unit page - Then I only see `Common Problem Types` and `Advanced` tabs in `problem` category - """ - self.go_to_unit_page() - page = ContainerPage(self.browser, None) - self.assertEqual(page.get_category_tab_names('problem'), ['Common Problem Types', 'Advanced']) - - def test_common_problem_types_tab(self): - """ - Scenario: Verify that correct components are present in Common Problem Types tab. - - Given I am a staff user - When I go to unit page - Then I see correct components under `Common Problem Types` tab in `problem` category - """ - self.go_to_unit_page() - page = ContainerPage(self.browser, None) - - expected_components = [ - "Blank Common Problem", - "Checkboxes", - "Dropdown", - "Multiple Choice", - "Numerical Input", - "Text Input", - "Checkboxes with Hints and Feedback", - "Dropdown with Hints and Feedback", - "Multiple Choice with Hints and Feedback", - "Numerical Input with Hints and Feedback", - "Text Input with Hints and Feedback", - ] - self.assertEqual(page.get_category_tab_components('problem', 1), expected_components) - - -@attr(shard=16) -@ddt.ddt -class MoveComponentTest(ContainerBase): - """ - Tests of moving an XBlock to another XBlock. - """ - PUBLISHED_LIVE_STATUS = "Publishing Status\nPublished and Live" - DRAFT_STATUS = "Publishing Status\nDraft (Unpublished changes)" - - def setUp(self, is_staff=True): - super(MoveComponentTest, self).setUp(is_staff=is_staff) - self.container = ContainerPage(self.browser, None) - self.move_modal_view = MoveModalView(self.browser) - - self.navigation_options = { - 'section': 0, - 'subsection': 0, - 'unit': 1, - } - self.source_component_display_name = 'HTML 11' - self.source_xblock_category = 'component' - self.message_move = u'Success! "{display_name}" has been moved.' - self.message_undo = u'Move cancelled. "{display_name}" has been moved back to its original location.' - - def populate_course_fixture(self, course_fixture): - """ - Sets up a course structure. - """ - # pylint: disable=attribute-defined-outside-init - self.unit_page1 = XBlockFixtureDesc('vertical', 'Test Unit 1').add_children( - XBlockFixtureDesc('html', 'HTML 11'), - XBlockFixtureDesc('html', 'HTML 12') - ) - self.unit_page2 = XBlockFixtureDesc('vertical', 'Test Unit 2').add_children( - XBlockFixtureDesc('html', 'HTML 21'), - XBlockFixtureDesc('html', 'HTML 22') - ) - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - self.unit_page1, - self.unit_page2 - ) - ) - ) - - def verify_move_opertions(self, unit_page, source_component, operation, component_display_names_after_operation, - should_verify_publish_title=True): - """ - Verify move operations. - - Arguments: - unit_page (Object) Unit container page. - source_component (Object) Source XBlock object to be moved. - operation (str), `move` or `undo move` operation. - component_display_names_after_operation (dict) Display names of components after operation in source/dest - should_verify_publish_title (Boolean) Should verify publish title ot not. Default is True. - """ - source_component.open_move_modal() - self.move_modal_view.navigate_to_category(self.source_xblock_category, self.navigation_options) - self.assertEqual(self.move_modal_view.is_move_button_enabled, True) - - # Verify unit is in published state before move operation - if should_verify_publish_title: - self.container.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - - self.move_modal_view.click_move_button() - self.container.verify_confirmation_message( - self.message_move.format(display_name=self.source_component_display_name) - ) - self.assertEqual(len(unit_page.displayed_children), 1) - - # Verify unit in draft state now - if should_verify_publish_title: - self.container.verify_publish_title(self.DRAFT_STATUS) - - if operation == 'move': - self.container.click_take_me_there_link() - elif operation == 'undo_move': - self.container.click_undo_move_link() - self.container.verify_confirmation_message( - self.message_undo.format(display_name=self.source_component_display_name) - ) - - unit_page = ContainerPage(self.browser, None) - components = unit_page.displayed_children - self.assertEqual( - [component.name for component in components], - component_display_names_after_operation - ) - - def verify_state_change(self, unit_page, operation): - """ - Verify that after state change, confirmation message is hidden. - - Arguments: - unit_page (Object) Unit container page. - operation (String) Publish or discard changes operation. - """ - # Verify unit in draft state now - self.container.verify_publish_title(self.DRAFT_STATUS) - - # Now click publish/discard button - if operation == 'publish': - unit_page.publish() - else: - unit_page.discard_changes() - - # Now verify success message is hidden - self.container.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - self.container.verify_confirmation_message( - message=self.message_move.format(display_name=self.source_component_display_name), - verify_hidden=True - ) - - def test_move_component_successfully(self): - """ - Test if we can move a component successfully. - - Given I am a staff user - And I go to unit page in first section - And I open the move modal - And I navigate to unit in second section - And I see move button is enabled - When I click on the move button - Then I see move operation success message - And When I click on take me there link - Then I see moved component there. - """ - unit_page = self.go_to_unit_page(unit_name='Test Unit 1') - components = unit_page.displayed_children - self.assertEqual(len(components), 2) - - self.verify_move_opertions( - unit_page=unit_page, - source_component=components[0], - operation='move', - component_display_names_after_operation=['HTML 21', 'HTML 22', 'HTML 11'] - ) - - @ddt.data('publish', 'discard') - def test_publish_discard_changes_afer_move(self, operation): - """ - Test if success banner is hidden when we discard changes or publish the unit after a move operation. - - Given I am a staff user - And I go to unit page in first section - And I open the move modal - And I navigate to unit in second section - And I see move button is enabled - When I click on the move button - Then I see move operation success message - And When I click on publish or discard changes button - Then I see move operation success message is hidden. - """ - unit_page = self.go_to_unit_page(unit_name='Test Unit 1') - components = unit_page.displayed_children - self.assertEqual(len(components), 2) - - components[0].open_move_modal() - self.move_modal_view.navigate_to_category(self.source_xblock_category, self.navigation_options) - self.assertEqual(self.move_modal_view.is_move_button_enabled, True) - - # Verify unit is in published state before move operation - self.container.verify_publish_title(self.PUBLISHED_LIVE_STATUS) - - self.move_modal_view.click_move_button() - self.container.verify_confirmation_message( - self.message_move.format(display_name=self.source_component_display_name) - ) - self.assertEqual(len(unit_page.displayed_children), 1) - - self.verify_state_change(unit_page, operation) - - def test_content_experiment(self): - """ - Test if we can move a component of content experiment successfully. - - Given that I am a staff user - And I go to content experiment page - And I open the move dialogue modal - When I navigate to the unit in second section - Then I see move button is enabled - And when I click on the move button - Then I see move operation success message - And when I click on take me there link - Then I see moved component there - And when I undo move a component - Then I see that undo move operation success message - """ - # Add content experiment support to course. - self.course_fixture.add_advanced_settings({ - u'advanced_modules': {'value': ['split_test']}, - }) - - # Create group configurations - # pylint: disable=protected-access - self.course_fixture._update_xblock(self.course_fixture._course_location, { - 'metadata': { - u'user_partitions': [ - create_user_partition_json( - 0, - 'Test Group Configuration', - 'Description of the group configuration.', - [Group('0', 'Group A'), Group('1', 'Group B')] - ), - ], - }, - }) - - # Add split test to unit_page1 and assign newly created group configuration to it - split_test = XBlockFixtureDesc('split_test', 'Test Content Experiment', metadata={'user_partition_id': 0}) - self.course_fixture.create_xblock(self.unit_page1.locator, split_test) - - # Visit content experiment container page. - unit_page = ContainerPage(self.browser, split_test.locator) - unit_page.visit() - - group_a_locator = unit_page.displayed_children[0].locator - - # Add some components to Group A. - self.course_fixture.create_xblock( - group_a_locator, XBlockFixtureDesc('html', 'HTML 311') - ) - self.course_fixture.create_xblock( - group_a_locator, XBlockFixtureDesc('html', 'HTML 312') - ) - - # Go to group page to move it's component. - group_container_page = ContainerPage(self.browser, group_a_locator) - group_container_page.visit() - - # Verify content experiment block has correct groups and components. - components = group_container_page.displayed_children - self.assertEqual(len(components), 2) - - self.source_component_display_name = 'HTML 311' - - # Verify undo move operation for content experiment. - self.verify_move_opertions( - unit_page=group_container_page, - source_component=components[0], - operation='undo_move', - component_display_names_after_operation=['HTML 311', 'HTML 312'], - should_verify_publish_title=False - ) - - # Verify move operation for content experiment. - self.verify_move_opertions( - unit_page=group_container_page, - source_component=components[0], - operation='move', - component_display_names_after_operation=['HTML 21', 'HTML 22', 'HTML 311'], - should_verify_publish_title=False - ) - - # Ideally this test should be decorated with @attr('a11y') so that it should run in a11y jenkins job - # But for some reason it always fails in a11y jenkins job and passes always locally on devstack as well - # as in bokchoy jenkins job. Due to this reason, test is marked to run under bokchoy jenkins job. - def test_a11y(self): - """ - Verify move modal a11y. - """ - unit_page = self.go_to_unit_page(unit_name='Test Unit 1') - - unit_page.a11y_audit.config.set_scope( - include=[".modal-window.move-modal"] - ) - unit_page.a11y_audit.config.set_rules({ - 'ignore': [ - 'color-contrast', # TODO: AC-716 - 'link-href', # TODO: AC-716 - ] - }) - - unit_page.displayed_children[0].open_move_modal() - - for category in ['section', 'subsection', 'component']: - self.move_modal_view.navigate_to_category(category, self.navigation_options) - unit_page.a11y_audit.check_for_accessibility_errors() diff --git a/common/test/acceptance/tests/studio/test_studio_discussion_component.py b/common/test/acceptance/tests/studio/test_studio_discussion_component.py deleted file mode 100644 index a8c1ce1922..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_discussion_component.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Acceptance tests for discussion component in studio -""" - - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.studio.container import ContainerPage -from common.test.acceptance.pages.studio.discussion_component_editor import DiscussionComponentEditor -from common.test.acceptance.pages.studio.utils import add_component -from common.test.acceptance.tests.studio.base_studio_test import ContainerBase - - -class DiscussionComponentTest(ContainerBase): - """ - Feature: CMS.Component Adding - As a course author, I want to be able to add and edit Discussion component - """ - shard = 20 - - def setUp(self, is_staff=True): - """ - Create a course with a section, subsection, and unit to which to add the component. - """ - super(DiscussionComponentTest, self).setUp(is_staff=is_staff) - self.component = 'discussion' - self.unit = self.go_to_unit_page() - self.container_page = ContainerPage(self.browser, None) - # Add Discussion component - add_component(self.container_page, 'discussion', self.component) - self.component = self.unit.xblocks[1] - self.container_page.edit() - self.discussion_editor = DiscussionComponentEditor(self.browser, self.component.locator) - - def populate_course_fixture(self, course_fixture): - """ - Adds a course fixture - """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ) - ) - - def test_view_discussion_component_metadata(self): - """ - Scenario: Staff user can view discussion component metadata - Given I am in Studio and I have added a Discussion component - When I edit Discussion component - Then I see three settings and their expected values - """ - - field_values = self.discussion_editor.edit_discussion_field_values - self.assertEqual( - field_values, - ['Discussion', 'Week 1', 'Topic-Level Student-Visible Label'] - ) - - def test_edit_discussion_component(self): - """ - Scenario: Staff user can modify display name - Given I am in Studio and I have added a Discussion component - When I open Discussion component's edit dialogue - Then I can modify the display name - And My display name change is persisted on save - """ - - field_name = 'Display Name' - new_name = 'Test Name' - self.discussion_editor.set_field_val(field_name, new_name) - self.discussion_editor.save() - component_name = self.unit.xblock_titles[0] - self.assertEqual(component_name, new_name) diff --git a/common/test/acceptance/tests/studio/test_studio_general.py b/common/test/acceptance/tests/studio/test_studio_general.py deleted file mode 100644 index e7e4676ca6..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_general.py +++ /dev/null @@ -1,268 +0,0 @@ -""" -Acceptance tests for Studio. -""" - - -import uuid - -from selenium.webdriver.common.keys import Keys - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.studio import LMS_URL -from common.test.acceptance.pages.studio.asset_index import AssetIndexPageStudioFrontend -from common.test.acceptance.pages.studio.course_info import CourseUpdatesPage -from common.test.acceptance.pages.studio.edit_tabs import PagesPage -from common.test.acceptance.pages.studio.import_export import ExportCoursePage, ImportCoursePage -from common.test.acceptance.pages.studio.index import AccessibilityPage, DashboardPage, HomePage, IndexPage -from common.test.acceptance.pages.studio.login import CourseOutlineSignInRedirectPage, LoginPage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage -from common.test.acceptance.pages.studio.settings import SettingsPage -from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettingsPage -from common.test.acceptance.pages.studio.settings_graders import GradingPage -from common.test.acceptance.pages.studio.signup import SignupPage -from common.test.acceptance.pages.studio.textbook_upload import TextbookUploadPage -from common.test.acceptance.pages.studio.users import CourseTeamPage -from common.test.acceptance.tests.helpers import AcceptanceTest, UniqueCourseTest - -from .base_studio_test import StudioCourseTest - - -class LoggedOutTest(AcceptanceTest): - """ - Smoke test for pages in Studio that are visible when logged out. - """ - shard = 21 - - def setUp(self): - super(LoggedOutTest, self).setUp() - self.pages = [LoginPage(self.browser), IndexPage(self.browser), SignupPage(self.browser), - AccessibilityPage(self.browser)] - - def test_page_existence(self): - """ - Make sure that all the pages are accessible. - Rather than fire up the browser just to check each url, - do them all sequentially in this testcase. - """ - for page in self.pages: - page.visit() - - -class LoggedInPagesTest(AcceptanceTest): - """ - Verify the pages in Studio that you can get to when logged in and do not have a course yet. - """ - shard = 21 - - def setUp(self): - super(LoggedInPagesTest, self).setUp() - self.auth_page = AutoAuthPage(self.browser, staff=True) - self.dashboard_page = DashboardPage(self.browser) - self.home_page = HomePage(self.browser) - - def test_logged_in_no_courses(self): - """ - Make sure that you can get to the dashboard and home pages without a course. - """ - self.auth_page.visit() - self.dashboard_page.visit() - self.home_page.visit() - - -class SignUpAndSignInTest(UniqueCourseTest): - """ - Test studio sign-up and sign-in - """ - shard = 21 - - def setUp(self): - super(SignUpAndSignInTest, self).setUp() - self.sign_up_page = SignupPage(self.browser) - self.login_page = LoginPage(self.browser) - - self.course_outline_page = CourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - self.course_outline_sign_in_redirect_page = CourseOutlineSignInRedirectPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self.course_fixture = CourseFixture( - self.course_info['org'], - self.course_info['number'], - self.course_info['run'], - self.course_info['display_name'], - ) - self.user = None - - def install_course_fixture(self): - """ - Install a course fixture - """ - self.course_fixture.install() - self.user = self.course_fixture.user - - def test_sign_up_from_home(self): - """ - Scenario: Sign up from the homepage - Given I visit the Studio homepage - When I click the link with the text "Sign Up" - And I fill in the registration form - And I press the Create My Account button on the registration form - Then I should see an email verification prompt - """ - index_page = IndexPage(self.browser) - index_page.visit() - index_page.click_sign_up() - - # Register the user. - unique_number = uuid.uuid4().hex[:4] - self.sign_up_page.sign_up_user( - '{}-email@host.com'.format(unique_number), - '{}-name'.format(unique_number), - '{}-username'.format(unique_number), - '{}-password'.format(unique_number), - ) - home = HomePage(self.browser) - home.wait_for_page() - - def test_sign_up_with_bad_password(self): - """ - Scenario: Sign up from the homepage - Given I visit the Studio homepage - When I click the link with the text "Sign Up" - And I fill in the registration form - When I enter an insufficient password and focus out - I should see an error message - """ - index_page = IndexPage(self.browser) - index_page.visit() - index_page.click_sign_up() - - password_input = self.sign_up_page.input_password('a') # Arbitrary short password that will fail - password_input.send_keys(Keys.TAB) # Focus out of the element - index_page.wait_for_element_visibility('#register-password-validation-error', 'Password Error Message') - self.assertIsNotNone(index_page.q(css='#register-password-validation-error-msg')) # Error message should exist - - def test_login_with_valid_redirect(self): - """ - Scenario: Login with a valid redirect - Given I have opened a new course in Studio - And I am not logged in - And I visit the url "/course/slashes:MITx+999+Robot_Super_Course" - And I should see the path is "/signin_redirect_to_lms?next=/course/slashes%3AMITx%2B999%2BRobot_Super_Course" - When I fill in and submit the LMS login form - Then I should see that the path is "/course/slashes:MITx+999+Robot_Super_Course" - """ - self.install_course_fixture() - # Get the url, browser should land here after sign in. - course_url = self.course_outline_sign_in_redirect_page.url - self.course_outline_sign_in_redirect_page.visit() - # Login - self.course_outline_sign_in_redirect_page.login(self.user['email'], self.user['password']) - self.course_outline_page.wait_for_page() - # Verify that correct course is displayed after sign in. - self.assertEqual(self.browser.current_url, course_url) - - -class CoursePagesTest(StudioCourseTest): - """ - Tests that verify the pages in Studio that you can get to when logged - in and have a course. - """ - shard = 21 - COURSE_ID_SEPARATOR = "." - - def setUp(self): - """ - Install a course with no content using a fixture. - """ - super(CoursePagesTest, self).setUp() - - self.pages = [ - clz(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']) - for clz in [ - AssetIndexPageStudioFrontend, - CourseUpdatesPage, - PagesPage, ExportCoursePage, ImportCoursePage, CourseTeamPage, CourseOutlinePage, SettingsPage, - AdvancedSettingsPage, GradingPage, TextbookUploadPage - ] - ] - - def test_page_redirect(self): - """ - /course/ is the base URL for all courses, but by itself, it should - redirect to /home/. - """ - self.dashboard_page = DashboardPage(self.browser) # pylint: disable=attribute-defined-outside-init - self.dashboard_page.visit() - self.assertEqual(self.browser.current_url.strip('/').rsplit('/')[-1], 'home') - - def test_page_existence(self): - """ - Make sure that all these pages are accessible once you have a course. - Rather than fire up the browser just to check each url, - do them all sequentially in this testcase. - """ - - # In the real workflow you will be at the dashboard page - # after you log in. This test was intermittently failing on the - # first (asset) page load with a 404. - # Not exactly sure why, so adding in a visit - # to the dashboard page here to replicate the usual flow. - self.dashboard_page = DashboardPage(self.browser) - self.dashboard_page.visit() - - # Verify that each page is available - for page in self.pages: - page.visit() - - -class DiscussionPreviewTest(StudioCourseTest): - """ - Tests that Inline Discussions are rendered with a custom preview in Studio - """ - shard = 21 - - def setUp(self): - super(DiscussionPreviewTest, self).setUp() - cop = CourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - cop.visit() - self.unit = cop.section('Test Section').subsection('Test Subsection').expand_subsection().unit('Test Unit') - self.unit.go_to() - - def populate_course_fixture(self, course_fixture): - """ - Return a test course fixture containing a discussion component. - """ - course_fixture.add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - XBlockFixtureDesc("vertical", "Test Unit").add_children( - XBlockFixtureDesc( - "discussion", - "Test Discussion", - ) - ) - ) - ) - ) - - def test_is_preview(self): - """ - Ensure that the preview version of the discussion is rendered. - """ - self.assertTrue(self.unit.q(css=".discussion-preview").present) - self.assertFalse(self.unit.q(css=".discussion-show").present) diff --git a/common/test/acceptance/tests/studio/test_studio_grading.py b/common/test/acceptance/tests/studio/test_studio_grading.py deleted file mode 100644 index 419d56d8b4..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_grading.py +++ /dev/null @@ -1,248 +0,0 @@ -""" -Acceptance tests for grade settings in Studio. -""" - - -from bok_choy.promise import EmptyPromise -from six.moves import range - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.studio.settings_graders import GradingPage -from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest - - -class GradingPageTest(StudioCourseTest): - """ - Bockchoy tests to add/edit grade settings in studio. - """ - shard = 23 - - url = None - GRACE_FIELD_CSS = "#course-grading-graceperiod" - - def setUp(self): # pylint: disable=arguments-differ - super(GradingPageTest, self).setUp() - self.grading_page = GradingPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self.grading_page.visit() - self.ensure_input_fields_are_loaded() - - def ensure_input_fields_are_loaded(self): - """ - Ensures values in input fields are loaded. - """ - EmptyPromise( - lambda: self.grading_page.q(css=self.GRACE_FIELD_CSS).attrs('value')[0], - "Waiting for input fields to be loaded" - ).fulfill() - - def populate_course_fixture(self, course_fixture): - """ - Return a test course fixture. - """ - course_fixture.add_children( - XBlockFixtureDesc("chapter", "Test Section").add_children( - XBlockFixtureDesc("sequential", "Test Subsection").add_children( - ) - ) - ) - - def test_add_grade_range(self): - """ - Scenario: Users can add grading ranges - Given I have opened a new course in Studio - And I am viewing the grading settings - When I add "1" new grade - Then I see I now have "3" - """ - length = self.grading_page.total_number_of_grades - self.grading_page.click_add_grade() - self.assertTrue(self.grading_page.is_grade_added(length)) - self.grading_page.save() - self.grading_page.refresh_and_wait_for_load() - total_number_of_grades = self.grading_page.total_number_of_grades - self.assertEqual(total_number_of_grades, 3) - - def test_staff_can_add_up_to_five_grades_only(self): - """ - Scenario: Users can only have up to 5 grading ranges - Given I have opened a new course in Studio - And I am viewing the grading settings - When I try to add more than 5 grades - Then I see I have only "5" grades - """ - for grade_ordinal in range(1, 5): - length = self.grading_page.total_number_of_grades - self.grading_page.click_add_grade() - # By default page has 2 grades, so greater than 3 means, attempt is made to add 6th grade - if grade_ordinal > 3: - self.assertFalse(self.grading_page.is_grade_added(length)) - else: - self.assertTrue(self.grading_page.is_grade_added(length)) - self.grading_page.save() - self.grading_page.refresh_and_wait_for_load() - total_number_of_grades = self.grading_page.total_number_of_grades - self.assertEqual(total_number_of_grades, 5) - - def test_grades_remain_consistent(self): - """ - Scenario: When user removes a grade the remaining grades should be consistent - Given I have opened a new course in Studio - And I am viewing the grading settings - When I add "2" new grade - Then Grade list has "A,B,C,F" grades - And I delete a grade - Then Grade list has "A,B,F" grades - """ - for _ in range(2): - length = self.grading_page.total_number_of_grades - self.grading_page.click_add_grade() - self.assertTrue(self.grading_page.is_grade_added(length)) - self.grading_page.save() - grades_alphabets = self.grading_page.grade_letters - self.assertEqual(grades_alphabets, ['A', 'B', 'C', 'F']) - self.grading_page.remove_grades(1) - self.grading_page.save() - grades_alphabets = self.grading_page.grade_letters - self.assertEqual(grades_alphabets, ['A', 'B', 'F']) - - def test_staff_can_delete_grade_range(self): - """ - Scenario: Users can delete grading ranges - Given I have opened a new course in Studio - And I am viewing the grading settings - When I add "1" new grade - And I delete a grade - Then I see I now have "2" grades - """ - length = self.grading_page.total_number_of_grades - self.grading_page.click_add_grade() - self.assertTrue(self.grading_page.is_grade_added(length)) - self.grading_page.save() - total_number_of_grades = self.grading_page.total_number_of_grades - self.assertEqual(total_number_of_grades, 3) - self.grading_page.remove_grades(1) - total_number_of_grades = self.grading_page.total_number_of_grades - self.assertEqual(total_number_of_grades, 2) - - def test_staff_can_move_grading_ranges(self): - """ - Scenario: Users can move grading ranges - Given I have opened a new course in Studio - And I am viewing the grading settings - When I move a grading section - Then I see that the grade range has changed - """ - grade_ranges = self.grading_page.grades_range - self.assertIn('0-50', grade_ranges) - self.grading_page.drag_and_drop_grade() - grade_ranges = self.grading_page.grades_range - self.assertIn( - '0-3', - grade_ranges, - u'expected range: 0-3, not found in grade ranges:{}'.format(grade_ranges) - ) - - def test_settings_are_persisted_on_save_only(self): - """ - Scenario: Settings are only persisted when saved - Given I have populated a new course in Studio - And I am viewing the grading settings - When I change assignment type "Homework" to "New Type" - Then I do not see the changes persisted on refresh - """ - self.grading_page.change_assignment_name('Homework', 'New Type') - self.grading_page.refresh_and_wait_for_load() - self.assertIn('Homework', self.grading_page.get_assignment_names) - - def test_settings_are_reset_on_cancel(self): - """ - Scenario: Settings are reset on cancel - Given I have populated a new course in Studio - And I am viewing the grading settings - When I change assignment type "Homework" to "New Type" - And I press the "Cancel" notification button - Then I see the assignment type "Homework" - """ - self.grading_page.change_assignment_name('Homework', 'New Type') - self.grading_page.cancel() - assignment_names = self.grading_page.get_assignment_names - self.assertIn('Homework', assignment_names) - - def test_confirmation_is_shown_on_save(self): - """ - Scenario: Confirmation is shown on save - Given I have populated a new course in Studio - And I am viewing the grading settings - When I change assignment type "Homework" to "New Type" - And I press the "Save" notification button - Then I see a confirmation that my changes have been saved - """ - self.grading_page.change_assignment_name('Homework', 'New Type') - self.grading_page.save() - confirmation_message = self.grading_page.confirmation_message - self.assertEqual(confirmation_message, 'Your changes have been saved.') - - def test_staff_can_set_weight_to_assignment(self): - """ - Scenario: Users can set weight to Assignment types - Given I have opened a new course in Studio - And I am viewing the grading settings - When I add a new assignment type "New Type" - And I set the assignment weight to "7" - And I press the "Save" notification button - Then the assignment weight is displayed as "7" - And I reload the page - Then the assignment weight is displayed as "7" - """ - self.grading_page.add_new_assignment_type() - self.grading_page.change_assignment_name('', 'New Type') - self.grading_page.set_weight('New Type', '7') - self.grading_page.save() - assignment_weight = self.grading_page.get_assignment_weight('New Type') - self.assertEqual(assignment_weight, '7') - self.grading_page.refresh_and_wait_for_load() - assignment_weight = self.grading_page.get_assignment_weight('New Type') - self.assertEqual(assignment_weight, '7') - - def test_staff_cannot_save_invalid_settings(self): - """ - Scenario: User cannot save invalid settings - Given I have populated a new course in Studio - And I am viewing the grading settings - When I change assignment type "Homework" to "" - Then the save notification button is disabled - """ - self.grading_page.change_assignment_name('Homework', '') - self.assertTrue(self.grading_page.is_notification_button_disbaled(), True) - - def test_edit_highest_grade_name(self): - """ - Scenario: User can edit grading range names - Given I have populated a new course in Studio - And I am viewing the grading settings - When I change the highest grade range to "Good" - And I press the "Save" notification button - And I reload the page - Then I see the highest grade range is "Good" - """ - self.grading_page.edit_grade_name('Good') - self.grading_page.save() - self.grading_page.refresh_and_wait_for_load() - grade_name = self.grading_page.highest_grade_name - self.assertEqual(grade_name, 'Good') - - def test_staff_cannot_edit_lowest_grade_name(self): - """ - Scenario: User cannot edit failing grade range name - Given I have populated a new course in Studio - And I am viewing the grading settings - Then I cannot edit the "Fail" grade range - """ - self.grading_page.try_edit_fail_grade('Failure') - self.assertNotEqual(self.grading_page.lowest_grade_name, 'Failure') diff --git a/common/test/acceptance/tests/studio/test_studio_home.py b/common/test/acceptance/tests/studio/test_studio_home.py deleted file mode 100644 index 2468f08d59..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_home.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -Acceptance tests for Home Page (My Courses / My Libraries). -""" - - -import datetime - -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage -from common.test.acceptance.pages.studio.index import DashboardPage -from common.test.acceptance.tests.helpers import AcceptanceTest, get_selected_option_text, select_option_by_text - -from .base_studio_test import StudioCourseTest - - -class CreateLibraryTest(AcceptanceTest): - """ - Test that we can create a new content library on the studio home page. - """ - - def setUp(self): - """ - Load the helper for the home page (dashboard page) - """ - super(CreateLibraryTest, self).setUp() - - self.auth_page = AutoAuthPage(self.browser, staff=True) - self.dashboard_page = DashboardPage(self.browser) - - -class StudioLanguageTest(AcceptanceTest): - """ Test suite for the Studio Language """ - shard = 21 - - def setUp(self): - super(StudioLanguageTest, self).setUp() - self.dashboard_page = DashboardPage(self.browser) - self.account_settings = AccountSettingsPage(self.browser) - AutoAuthPage(self.browser).visit() - - def test_studio_language_change(self): - """ - Scenario: Ensure that language selection is working fine. - First I go to the user dashboard page in studio. I can see 'English' is selected by default. - Then I choose 'Dummy Language' from drop down (at top of the page). - Then I visit the student account settings page and I can see the language has been updated to 'Dummy Language' - in both drop downs. - """ - dummy_language = u'Dummy Language (Esperanto)' - self.dashboard_page.visit() - language_selector = self.dashboard_page.language_selector - self.assertEqual( - get_selected_option_text(language_selector), - u'English' - ) - - select_option_by_text(language_selector, dummy_language) - self.dashboard_page.wait_for_ajax() - self.account_settings.visit() - self.assertEqual(self.account_settings.value_for_dropdown_field('pref-lang'), dummy_language) - self.assertEqual( - get_selected_option_text(language_selector), - u'Dummy Language (Esperanto)' - ) - - -class ArchivedCourseTest(StudioCourseTest): - """ Tests that archived courses appear in their own list. """ - - def setUp(self, is_staff=True, test_xss=False): - """ - Load the helper for the home page (dashboard page) - """ - super(ArchivedCourseTest, self).setUp(is_staff=is_staff, test_xss=test_xss) - self.dashboard_page = DashboardPage(self.browser) - - def populate_course_fixture(self, course_fixture): - current_time = datetime.datetime.now() - course_start_date = current_time - datetime.timedelta(days=60) - course_end_date = current_time - datetime.timedelta(days=90) - - course_fixture.add_course_details({ - 'start_date': course_start_date, - 'end_date': course_end_date - }) diff --git a/common/test/acceptance/tests/studio/test_studio_html_editor.py b/common/test/acceptance/tests/studio/test_studio_html_editor.py deleted file mode 100644 index 138a73cdb9..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_html_editor.py +++ /dev/null @@ -1,399 +0,0 @@ -""" -Acceptance tests for HTML component in studio -""" - - -import os - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.studio.container import ContainerPage, XBlockWrapper -from common.test.acceptance.pages.studio.html_component_editor import HTMLEditorIframe, HtmlXBlockEditorView -from common.test.acceptance.pages.studio.utils import add_component, type_in_codemirror -from common.test.acceptance.tests.studio.base_studio_test import ContainerBase - -UPLOAD_SUFFIX = 'data/uploads/studio-uploads/' -UPLOAD_FILE_DIR = os.path.abspath(os.path.join(__file__, '../../../../', UPLOAD_SUFFIX)) - - -class HTMLComponentEditorTests(ContainerBase): - """ - Feature: CMS.Component Adding - As a course author, I want to be able to add and edit HTML component - """ - shard = 15 - - def setUp(self, is_staff=True): - """ - Create a course with a section, subsection, and unit to which to add the component. - """ - super(HTMLComponentEditorTests, self).setUp(is_staff=is_staff) - self.unit = self.go_to_unit_page() - self.container_page = ContainerPage(self.browser, None) - self.xblock_wrapper = XBlockWrapper(self.browser, None) - self.component = None - self.html_editor = None - self.iframe = None - - def populate_course_fixture(self, course_fixture): - """ - Adds a course fixture - """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ) - ) - - def _add_content(self, content): - """ - Set and save content in editor and assert its presence in container page's html - - Args: - content(str): Verifiable content - """ - self.html_editor.set_raw_content(content) - self.html_editor.save_content() - self.container_page.wait_for_page() - - def _add_component(self, sub_type): - """ - Add sub-type of HTML component in studio - - Args: - sub_type(str): Sub-type of HTML component - """ - add_component(self.container_page, 'html', sub_type) - self.component = self.unit.xblocks[1] - self.html_editor = HtmlXBlockEditorView(self.browser, self.component.locator) - self.iframe = HTMLEditorIframe(self.browser, self.component.locator) - - def test_user_can_view_metadata(self): - """ - Scenario: User can view metadata - Given I have created a Blank HTML Page - And I edit and select Settings - Then I see the HTML component settings - """ - - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self.html_editor.open_settings_tab() - display_name_value = self.html_editor.get_default_settings()[0] - display_name_key = self.html_editor.keys[0] - self.assertEqual( - ['Display Name', 'Text'], - [display_name_key, display_name_value], - "Settings not found" - ) - editor_value = self.html_editor.get_default_settings()[1] - editor_key = self.html_editor.keys[1] - self.assertEqual( - ['Editor', 'Visual'], - [editor_key, editor_value], - "Settings not found" - ) - - def test_user_can_modify_display_name(self): - """ - Scenario: User can modify display name - Given I have created a Blank HTML Page - And I edit and select Settings - Then I can modify the display name - And my display name change is persisted on save - """ - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self.html_editor.open_settings_tab() - self.html_editor.set_field_val('Display Name', 'New Name') - self.html_editor.save_settings() - component_name = self.unit.xblock_titles[0] - self.assertEqual(component_name, 'New Name', "Component name is not as edited") - - def test_link_plugin_sets_url_correctly(self): - """ - Scenario: TinyMCE link plugin sets urls correctly - Given I have created a Blank HTML Page - When I edit the page - And I add a link with static link "/static/image.jpg" via the Link Plugin Icon - Then the href link is rewritten to the asset link "image.jpg" - And the link is shown as "/static/image.jpg" in the Link Plugin - """ - static_link = '/static/image.jpg' - - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self.html_editor.open_link_plugin() - self.html_editor.save_static_link(static_link) - self.html_editor.switch_to_iframe() - href = self.iframe.href - self.assertIn('image.jpg', href) - self.iframe.select_link() - self.iframe.switch_to_default() - self.assertEqual( - self.html_editor.url_from_the_link_plugin, - static_link, - "URL in the link plugin is different" - ) - - def test_tinymce_and_codemirror_preserve_style_tags(self): - """ - Scenario: TinyMCE and CodeMirror preserve style tags - Given I have created a Blank HTML Page - When I edit the page - And type "

pages

" in the code editor and - press OK - And I save the page - Then the page text contains: - "" -

pages

- - "" - """ - content = u'

pages

' - - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self._add_content(content) - html = self.container_page.content_html - self.assertIn(content, html) - - def test_tinymce_and_codemirror_preserve_span_tags(self): - """ - Scenario: TinyMCE and CodeMirror preserve span tags - Given I have created a Blank HTML Page - When I edit the page - And type "Test" in the code editor and press OK - And I save the page - Then the page text contains: - "" - Test - "" - """ - content = "Test" - - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self._add_content(content) - html = self.container_page.content_html - self.assertIn(content, html) - - def test_tinymce_and_codemirror_preserve_math_tags(self): - """ - Scenario: TinyMCE and CodeMirror preserve math tags - Given I have created a Blank HTML Page - When I edit the page - And type "x2" in the code editor and press OK - And I save the page - Then the page text contains: - "" - x2 - "" - """ - content = "x2" - - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self._add_content(content) - html = self.container_page.content_html - self.assertIn(content, html) - - def test_code_format_toolbar_wraps_text_with_code_tags(self): - """ - Scenario: Code format toolbar button wraps text with code tags - Given I have created a Blank HTML Page - When I edit the page - And I set the text to "display as code" and I select the text - And I save the page - Then the page text contains: - "" -

display as code

- "" - """ - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self.html_editor.set_text_and_select("display as code") - self.html_editor.click_code_toolbar_button() - self.html_editor.save_content() - html = self.container_page.content_html - self.assertIn(html, '

display as code

') - - def test_raw_html_component_does_not_change_text(self): - """ - Scenario: Raw HTML component does not change text - Given I have created a raw HTML component - When I edit the page - And type "
  • zzzz
      " into the Raw Editor - And I save the page - Then the page text contains: - "" -
    1. zzzz
        - "" - And I edit the page - Then the Raw Editor contains exactly: - "" -
      1. zzzz
          - "" - """ - content = "
        1. zzzz
        2. " - - # Add Raw HTML type component - self._add_component('Raw HTML') - self.container_page.edit() - - # Set content in tinymce editor - type_in_codemirror(self.html_editor, 0, content) - self.html_editor.save_content() - - # The HTML of the content added through tinymce editor - html = self.container_page.content_html - # The text content should be present with its tag preserved - self.assertIn(content, html) - - self.container_page.edit() - editor_value = self.html_editor.editor_value - # The tinymce editor value should not be different from the content added in the start - self.assertEqual(content, editor_value) - - def test_tinymce_toolbar_buttons_are_as_expected(self): - """ - Scenario: TinyMCE toolbar buttons are as expected - Given I have created a Blank HTML Page - When I edit the page - Then the expected toolbar buttons are displayed - """ - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - - expected_buttons = [ - u'bold', - u'italic', - u'underline', - u'forecolor', - # This is our custom "code style" button, which uses an image instead of a class. - u'none', - u'alignleft', - u'aligncenter', - u'alignright', - u'alignjustify', - u'bullist', - u'numlist', - u'outdent', - u'indent', - u'blockquote', - u'link', - u'unlink', - u'image' - ] - toolbar_dropdowns = self.html_editor.toolbar_dropdown_titles - # The toolbar is divided in two sections: drop-downs and all other formatting buttons - # The assertions under asserts for the drop-downs - self.assertEqual(len(toolbar_dropdowns), 2) - self.assertEqual(['Paragraph', 'Font Family'], toolbar_dropdowns) - - toolbar_buttons = self.html_editor.toolbar_button_titles - # The assertions under asserts for all the remaining formatting buttons - self.assertEqual(len(toolbar_buttons), len(expected_buttons)) - - for index, button in enumerate(expected_buttons): - class_name = toolbar_buttons[index] - self.assertEqual("mce-ico mce-i-" + button, class_name) - - def test_static_links_converted(self): - """ - Scenario: Static links are converted when switching between code editor and WYSIWYG views - Given I have created a Blank HTML Page - When I edit the page - And type "" in the code editor and press OK - Then the src link is rewritten to the asset link /asset-v1:(course_id)+type@asset+block/image.jpg - And the code editor displays "

          " - """ - value = '' - - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - self.html_editor.set_raw_content(value) - self.html_editor.save_content() - html = self.container_page.content_html - src = "/asset-v1:{}+type@asset+block/image.jpg".format(self.course_id.strip('course-v1:')) - self.assertIn(src, html) - self.container_page.edit() - self.html_editor.open_raw_editor() - editor_value = self.html_editor.editor_value - self.assertEqual(value, editor_value) - - def test_font_selection_dropdown(self): - """ - Scenario: Font selection dropdown contains Default font and tinyMCE builtin fonts - Given I have created a Blank HTML Page - When I edit the page - And I click font selection dropdown - Then I should see a list of available fonts - And "Default" fonts should be available - And all standard tinyMCE fonts should be available - """ - # Add HTML Text type component - self._add_component('Text') - self.container_page.edit() - EXPECTED_FONTS = { - u"Default": [u'"Open Sans"', u'Verdana', u'Arial', u'Helvetica', u'sans-serif'], - u"Andale Mono": [u'andale mono', u'times'], - u"Arial": [u'arial', u'helvetica', u'sans-serif'], - u"Arial Black": [u'arial black', u'avant garde'], - u"Book Antiqua": [u'book antiqua', u'palatino'], - u"Comic Sans MS": [u'comic sans ms', u'sans-serif'], - u"Courier New": [u'courier new', u'courier'], - u"Georgia": [u'georgia', u'palatino'], - u"Helvetica": [u'helvetica'], - u"Impact": [u'impact', u'chicago'], - u"Symbol": [u'symbol'], - u"Tahoma": [u'tahoma', u'arial', u'helvetica', u'sans-serif'], - u"Terminal": [u'terminal', u'monaco'], - u"Times New Roman": [u'times new roman', u'times'], - u"Trebuchet MS": [u'trebuchet ms', u'geneva'], - u"Verdana": [u'verdana', u'geneva'], - # tinyMCE does not set font-family on dropdown span for these two fonts - u"Webdings": [u""], # webdings - u"Wingdings": [u""] # wingdings - } - self.html_editor.open_font_dropdown() - self.assertDictContainsSubset(EXPECTED_FONTS, self.html_editor.font_dict()) - - def test_image_modal(self): - """ - Scenario: TinyMCE text editor allows to add multiple images. - - Given I have created a Blank text editor Page. - I add an image in TinyMCE text editor and hit save button. - I edit the component again. - I add another image in TinyMCE text editor and hit save button again. - Then it is expected that both images show up on page. - """ - image_file_names = [u'file-0.png', u'file-1.png'] - self._add_component('Text') - - for image in image_file_names: - image_path = os.path.join(UPLOAD_FILE_DIR, image) - self.container_page.edit() - self.html_editor.open_image_modal() - self.html_editor.upload_image(image_path) - self.html_editor.save_content() - self.html_editor.wait_for_ajax() - - self.container_page.edit() - self.html_editor.open_raw_editor() - editor_value = self.html_editor.editor_value - number_of_images = editor_value.count(u'img') - self.assertEqual(number_of_images, 2) diff --git a/common/test/acceptance/tests/studio/test_studio_library.py b/common/test/acceptance/tests/studio/test_studio_library.py index aaf720c9f0..3ae657c0d3 100644 --- a/common/test/acceptance/tests/studio/test_studio_library.py +++ b/common/test/acceptance/tests/studio/test_studio_library.py @@ -3,150 +3,11 @@ Acceptance tests for Content Libraries in Studio """ -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.studio.library import LibraryEditPage -from common.test.acceptance.pages.studio.users import LibraryUsersPage from common.test.acceptance.tests.studio.base_studio_test import StudioLibraryTest from openedx.core.lib.tests import attr -@attr(shard=21) -class LibraryUsersPageTest(StudioLibraryTest): - """ - Test the functionality of the library "Instructor Access" page. - """ - def setUp(self): - super(LibraryUsersPageTest, self).setUp() - - # Create a second user for use in these tests: - AutoAuthPage(self.browser, username="second", email="second@example.com", no_login=True).visit() - - self.page = LibraryUsersPage(self.browser, self.library_key) - self.page.visit() - - def _refresh_page(self): - """ - Reload the page. - """ - self.page = LibraryUsersPage(self.browser, self.library_key) - self.page.visit() - - def test_user_management(self): - """ - Scenario: Ensure that we can edit the permissions of users. - Given I have a library in Studio where I am the only admin - assigned (which is the default for a newly-created library) - And I navigate to Library "Instructor Access" Page in Studio - Then there should be one user listed (myself), and I must - not be able to remove myself or my instructor privilege. - - When I click Add Instructor - Then I see a form to complete - When I complete the form and submit it - Then I can see the new user is listed as a "User" of the library - - When I click to Add Staff permissions to the new user - Then I can see the new user has staff permissions and that I am now - able to promote them to an Admin or remove their staff permissions. - - When I click to Add Admin permissions to the new user - Then I can see the new user has admin permissions and that I can now - remove Admin permissions from either user. - """ - def check_is_only_admin(user): - """ - Ensure user is an admin user and cannot be removed. - (There must always be at least one admin user.) - """ - self.assertIn("admin", user.role_label.lower()) - self.assertFalse(user.can_promote) - self.assertFalse(user.can_demote) - self.assertFalse(user.can_delete) - self.assertTrue(user.has_no_change_warning) - self.assertIn("Promote another member to Admin to remove your admin rights", user.no_change_warning_text) - - self.assertEqual(len(self.page.users), 1) - user = self.page.users[0] - self.assertTrue(user.is_current_user) - check_is_only_admin(user) - - # Add a new user: - - self.assertTrue(self.page.has_add_button) - self.assertFalse(self.page.new_user_form_visible) - self.page.click_add_button() - self.assertTrue(self.page.new_user_form_visible) - self.page.set_new_user_email('second@example.com') - self.page.click_submit_new_user_form() - - # Check the new user's listing: - - def get_two_users(): - """ - Expect two users to be listed, one being me, and another user. - Returns me, them - """ - users = self.page.users - self.assertEqual(len(users), 2) - self.assertEqual(len([u for u in users if u.is_current_user]), 1) - if users[0].is_current_user: - return users[0], users[1] - else: - return users[1], users[0] - - self._refresh_page() - user_me, them = get_two_users() - check_is_only_admin(user_me) - - self.assertIn("user", them.role_label.lower()) - self.assertTrue(them.can_promote) - self.assertIn("Add Staff Access", them.promote_button_text) - self.assertFalse(them.can_demote) - self.assertTrue(them.can_delete) - self.assertFalse(them.has_no_change_warning) - - # Add Staff permissions to the new user: - - them.click_promote() - self._refresh_page() - user_me, them = get_two_users() - check_is_only_admin(user_me) - - self.assertIn("staff", them.role_label.lower()) - self.assertTrue(them.can_promote) - self.assertIn("Add Admin Access", them.promote_button_text) - self.assertTrue(them.can_demote) - self.assertIn("Remove Staff Access", them.demote_button_text) - self.assertTrue(them.can_delete) - self.assertFalse(them.has_no_change_warning) - - # Add Admin permissions to the new user: - - them.click_promote() - self._refresh_page() - user_me, them = get_two_users() - self.assertIn("admin", user_me.role_label.lower()) - self.assertFalse(user_me.can_promote) - self.assertTrue(user_me.can_demote) - self.assertTrue(user_me.can_delete) - self.assertFalse(user_me.has_no_change_warning) - - self.assertIn("admin", them.role_label.lower()) - self.assertFalse(them.can_promote) - self.assertTrue(them.can_demote) - self.assertIn("Remove Admin Access", them.demote_button_text) - self.assertTrue(them.can_delete) - self.assertFalse(them.has_no_change_warning) - - # Delete the new user: - - them.click_delete() - self._refresh_page() - self.assertEqual(len(self.page.users), 1) - user = self.page.users[0] - self.assertTrue(user.is_current_user) - - @attr('a11y') class StudioLibraryA11yTest(StudioLibraryTest): """ diff --git a/common/test/acceptance/tests/studio/test_studio_library_container.py b/common/test/acceptance/tests/studio/test_studio_library_container.py deleted file mode 100644 index 38c459626d..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_library_container.py +++ /dev/null @@ -1,176 +0,0 @@ -""" -Acceptance tests for Library Content in LMS -""" - - -import textwrap - -import ddt -import six - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.studio.library import StudioLibraryContainerXBlockWrapper, StudioLibraryContentEditor -from common.test.acceptance.pages.studio.overview import CourseOutlinePage -from common.test.acceptance.tests.helpers import TestWithSearchIndexMixin, UniqueCourseTest -from common.test.acceptance.tests.studio.base_studio_test import StudioLibraryTest - -SECTION_NAME = 'Test Section' -SUBSECTION_NAME = 'Test Subsection' -UNIT_NAME = 'Test Unit' - - -@ddt.ddt -class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest, TestWithSearchIndexMixin): - """ - Test Library Content block in LMS - """ - shard = 17 - - def setUp(self): - """ - Install library with some content and a course using fixtures - """ - self._create_search_index() - super(StudioLibraryContainerTest, self).setUp() - # Also create a course: - self.course_fixture = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - self.populate_course_fixture(self.course_fixture) - self.course_fixture.install() - self.outline = CourseOutlinePage( - self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'] - ) - - self.outline.visit() - subsection = self.outline.section(SECTION_NAME).subsection(SUBSECTION_NAME) - self.unit_page = subsection.expand_subsection().unit(UNIT_NAME).go_to() - - def tearDown(self): - """ Tear down method: remove search index backing file """ - self._cleanup_index_file() - super(StudioLibraryContainerTest, self).tearDown() - - def populate_library_fixture(self, library_fixture): - """ - Populate the children of the test course fixture. - """ - library_fixture.add_children( - XBlockFixtureDesc("html", "Html1"), - XBlockFixtureDesc("html", "Html2"), - XBlockFixtureDesc("html", "Html3"), - ) - - def populate_course_fixture(self, course_fixture): - """ Install a course with sections/problems, tabs, updates, and handouts """ - library_content_metadata = { - 'source_library_id': six.text_type(self.library_key), - 'mode': 'random', - 'max_count': 1, - } - - course_fixture.add_children( - XBlockFixtureDesc('chapter', SECTION_NAME).add_children( - XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children( - XBlockFixtureDesc('vertical', UNIT_NAME).add_children( - XBlockFixtureDesc('library_content', "Library Content", metadata=library_content_metadata) - ) - ) - ) - ) - - def _get_library_xblock_wrapper(self, xblock): - """ - Wraps xblock into :class:`...pages.studio.library.StudioLibraryContainerXBlockWrapper` - """ - return StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(xblock) - - @ddt.data(1, 2, 3) - def test_can_edit_metadata(self, max_count): - """ - Scenario: Given I have a library, a course and library content xblock in a course - When I go to studio unit page for library content block - And I edit library content metadata and save it - Then I can ensure that data is persisted - """ - library_name = self.library_info['display_name'] - library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[1]) - library_container.edit() - edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator) - edit_modal.library_name = library_name - edit_modal.count = max_count - - library_container.save_settings() # saving settings - - # open edit window again to verify changes are persistent - library_container.edit() - edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator) - self.assertEqual(edit_modal.library_name, library_name) - self.assertEqual(edit_modal.count, max_count) - - def test_no_content_message(self): - """ - Scenario: Given I have a library, a course and library content xblock in a course - When I go to studio unit page for library content block - And I set Problem Type selector so that no libraries have matching content - Then I can see that "No matching content" warning is shown - When I set Problem Type selector so that there is matching content - Then I can see that warning messages are not shown - """ - # Add a single "Dropdown" type problem to the library (which otherwise has only HTML blocks): - self.library_fixture.create_xblock(self.library_fixture.library_location, XBlockFixtureDesc( - "problem", "Dropdown", - data=textwrap.dedent(""" - -

          Dropdown

          - -
          - """) - )) - - expected_text = 'There are no matching problem types in the specified libraries. Select another problem type' - - library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[1]) - - # precondition check - assert library has children matching filter criteria - self.assertFalse(library_container.has_validation_error) - self.assertFalse(library_container.has_validation_warning) - - library_container.edit() - edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator) - self.assertEqual(edit_modal.capa_type, "Any Type") # precondition check - edit_modal.capa_type = "Custom Evaluated Script" - - library_container.save_settings() - - self.assertTrue(library_container.has_validation_warning) - self.assertIn(expected_text, library_container.validation_warning_text) - - library_container.edit() - edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator) - self.assertEqual(edit_modal.capa_type, "Custom Evaluated Script") # precondition check - edit_modal.capa_type = "Dropdown" - library_container.save_settings() - - # Library should contain single Dropdown problem, so now there should be no errors again - self.assertFalse(library_container.has_validation_error) - self.assertFalse(library_container.has_validation_warning) - - def test_cannot_manage(self): - """ - Scenario: Given I have a library, a course and library content xblock in a course - When I go to studio unit page for library content block - And when I click the "View" link - Then I can see a preview of the blocks drawn from the library. - - And I do not see a duplicate button - And I do not see a delete button - """ - block_wrapper_unit_page = self._get_library_xblock_wrapper(self.unit_page.xblocks[0].children[0]) - container_page = block_wrapper_unit_page.go_to_container() - - for block in container_page.xblocks: - self.assertFalse(block.has_duplicate_button) - self.assertFalse(block.has_delete_button) - self.assertFalse(block.has_edit_visibility_button) diff --git a/common/test/acceptance/tests/studio/test_studio_outline.py b/common/test/acceptance/tests/studio/test_studio_outline.py deleted file mode 100644 index f089114ed2..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_outline.py +++ /dev/null @@ -1,1983 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Acceptance tests for studio related to the outline page. -""" - - -import itertools -import json -from datetime import datetime, timedelta -from unittest import skip - -from pytz import UTC -import six -from six.moves import range - -from common.test.acceptance.fixtures.config import ConfigModelFixture -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.lms.course_home import CourseHomePage -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.progress import ProgressPage -from common.test.acceptance.pages.studio.checklists import CourseChecklistsPage -from common.test.acceptance.pages.studio.overview import ContainerPage, CourseOutlinePage, ExpandCollapseLinkState -from common.test.acceptance.pages.studio.settings import SettingsPage -from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettingsPage -from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage -from common.test.acceptance.pages.studio.utils import add_discussion, drag, verify_ordering -from common.test.acceptance.tests.helpers import disable_animations, load_data_str -from openedx.core.lib.tests import attr - -from .base_studio_test import StudioCourseTest - -SECTION_NAME = 'Test Section' -SUBSECTION_NAME = 'Test Subsection' -UNIT_NAME = 'Test Unit' - - -class CourseOutlineTest(StudioCourseTest): - """ - Base class for all course outline tests - """ - - def setUp(self): - """ - Install a course with no content using a fixture. - """ - super(CourseOutlineTest, self).setUp() - self.course_outline_page = CourseOutlinePage( - self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'] - ) - self.advanced_settings = AdvancedSettingsPage( - self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'] - ) - - def populate_course_fixture(self, course_fixture): - """ Install a course with sections/problems, tabs, updates, and handouts """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', SECTION_NAME).add_children( - XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children( - XBlockFixtureDesc('vertical', UNIT_NAME).add_children( - XBlockFixtureDesc('problem', 'Test Problem 1', data=load_data_str('multiple_choice.xml')), - XBlockFixtureDesc('html', 'Test HTML Component'), - XBlockFixtureDesc('discussion', 'Test Discussion Component') - ) - ) - ) - ) - - def do_action_and_verify(self, outline_page, action, expected_ordering): - """ - Perform the supplied action and then verify the resulting ordering. - """ - if outline_page is None: - outline_page = self.course_outline_page.visit() - - action(outline_page) - verify_ordering(self, outline_page, expected_ordering) - - # Reload the page and expand all subsections to see that the change was persisted. - outline_page = self.course_outline_page.visit() - outline_page.q(css='.outline-item.outline-subsection.is-collapsed .ui-toggle-expansion').click() - verify_ordering(self, outline_page, expected_ordering) - - -@attr(shard=20) -class CourseOutlineDragAndDropTest(CourseOutlineTest): - """ - Tests of drag and drop within the outline page. - """ - __test__ = True - - def populate_course_fixture(self, course_fixture): - """ - Create a course with one section, two subsections, and four units - """ - # with collapsed outline - self.chap_1_handle = 0 - self.chap_1_seq_1_handle = 1 - - # with first sequential expanded - self.seq_1_vert_1_handle = 2 - self.seq_1_vert_2_handle = 3 - self.chap_1_seq_2_handle = 4 - - course_fixture.add_children( - XBlockFixtureDesc('chapter', "1").add_children( - XBlockFixtureDesc('sequential', '1.1').add_children( - XBlockFixtureDesc('vertical', '1.1.1'), - XBlockFixtureDesc('vertical', '1.1.2') - ), - XBlockFixtureDesc('sequential', '1.2').add_children( - XBlockFixtureDesc('vertical', '1.2.1'), - XBlockFixtureDesc('vertical', '1.2.2') - ) - ) - ) - - def drag_and_verify(self, source, target, expected_ordering, outline_page=None): - self.do_action_and_verify( - outline_page, - lambda outline: drag(outline, source, target), - expected_ordering - ) - - @skip("Fails in Firefox 45 but passes in Chrome") - def test_drop_unit_in_collapsed_subsection(self): - """ - Drag vertical "1.1.2" from subsection "1.1" into collapsed subsection "1.2" which already - have its own verticals. - """ - course_outline_page = self.course_outline_page.visit() - # expand first subsection - course_outline_page.q(css='.outline-item.outline-subsection.is-collapsed .ui-toggle-expansion').first.click() - - expected_ordering = [{"1": ["1.1", "1.2"]}, - {"1.1": ["1.1.1"]}, - {"1.2": ["1.1.2", "1.2.1", "1.2.2"]}] - self.drag_and_verify(self.seq_1_vert_2_handle, self.chap_1_seq_2_handle, expected_ordering, course_outline_page) - - -@attr(shard=3) -class WarningMessagesTest(CourseOutlineTest): - """ - Feature: Warning messages on sections, subsections, and units - """ - - __test__ = True - - STAFF_ONLY_WARNING = 'Contains staff only content' - LIVE_UNPUBLISHED_WARNING = 'Unpublished changes to live content' - FUTURE_UNPUBLISHED_WARNING = 'Unpublished changes to content that will release in the future' - NEVER_PUBLISHED_WARNING = 'Unpublished units will not be released' - - class PublishState(object): - """ - Default values for representing the published state of a unit - """ - NEVER_PUBLISHED = 1 - UNPUBLISHED_CHANGES = 2 - PUBLISHED = 3 - VALUES = [NEVER_PUBLISHED, UNPUBLISHED_CHANGES, PUBLISHED] - - class UnitState(object): - """ Represents the state of a unit """ - - def __init__(self, is_released, publish_state, is_locked): - """ Creates a new UnitState with the given properties """ - self.is_released = is_released - self.publish_state = publish_state - self.is_locked = is_locked - - @property - def name(self): - """ Returns an appropriate name based on the properties of the unit """ - result = "Released " if self.is_released else "Unreleased " - if self.publish_state == WarningMessagesTest.PublishState.NEVER_PUBLISHED: - result += "Never Published " - elif self.publish_state == WarningMessagesTest.PublishState.UNPUBLISHED_CHANGES: - result += "Unpublished Changes " - else: - result += "Published " - result += "Locked" if self.is_locked else "Unlocked" - return result - - def populate_course_fixture(self, course_fixture): - """ Install a course with various configurations that could produce warning messages """ - - # Define the dimensions that map to the UnitState constructor - features = [ - [True, False], # Possible values for is_released - self.PublishState.VALUES, # Possible values for publish_state - [True, False] # Possible values for is_locked - ] - - # Add a fixture for every state in the product of features - course_fixture.add_children(*[ - self._build_fixture(self.UnitState(*state)) for state in itertools.product(*features) - ]) - - def _build_fixture(self, unit_state): - """ Returns an XBlockFixtureDesc with a section, subsection, and possibly unit that has the given state. """ - name = unit_state.name - start = (datetime(1984, 3, 4) if unit_state.is_released else datetime.now(UTC) + timedelta(1)).isoformat() - - subsection = XBlockFixtureDesc('sequential', name, metadata={'start': start}) - - # Children of never published subsections will be added on demand via _ensure_unit_present - return XBlockFixtureDesc('chapter', name).add_children( - subsection if unit_state.publish_state == self.PublishState.NEVER_PUBLISHED - else subsection.add_children( - XBlockFixtureDesc('vertical', name, metadata={ - 'visible_to_staff_only': True if unit_state.is_locked else None - }) - ) - ) - - def test_released_never_published_locked(self): - """ Tests that released never published locked units display staff only warnings """ - self._verify_unit_warning( - self.UnitState(is_released=True, publish_state=self.PublishState.NEVER_PUBLISHED, is_locked=True), - self.STAFF_ONLY_WARNING - ) - - def test_released_never_published_unlocked(self): - """ Tests that released never published unlocked units display 'Unpublished units will not be released' """ - self._verify_unit_warning( - self.UnitState(is_released=True, publish_state=self.PublishState.NEVER_PUBLISHED, is_locked=False), - self.NEVER_PUBLISHED_WARNING - ) - - def test_released_unpublished_changes_locked(self): - """ Tests that released unpublished changes locked units display staff only warnings """ - self._verify_unit_warning( - self.UnitState(is_released=True, publish_state=self.PublishState.UNPUBLISHED_CHANGES, is_locked=True), - self.STAFF_ONLY_WARNING - ) - - def test_released_unpublished_changes_unlocked(self): - """ Tests that released unpublished changes unlocked units display 'Unpublished changes to live content' """ - self._verify_unit_warning( - self.UnitState(is_released=True, publish_state=self.PublishState.UNPUBLISHED_CHANGES, is_locked=False), - self.LIVE_UNPUBLISHED_WARNING - ) - - def test_released_published_locked(self): - """ Tests that released published locked units display staff only warnings """ - self._verify_unit_warning( - self.UnitState(is_released=True, publish_state=self.PublishState.PUBLISHED, is_locked=True), - self.STAFF_ONLY_WARNING - ) - - def test_released_published_unlocked(self): - """ Tests that released published unlocked units display no warnings """ - self._verify_unit_warning( - self.UnitState(is_released=True, publish_state=self.PublishState.PUBLISHED, is_locked=False), - None - ) - - def test_unreleased_never_published_locked(self): - """ Tests that unreleased never published locked units display staff only warnings """ - self._verify_unit_warning( - self.UnitState(is_released=False, publish_state=self.PublishState.NEVER_PUBLISHED, is_locked=True), - self.STAFF_ONLY_WARNING - ) - - def test_unreleased_never_published_unlocked(self): - """ Tests that unreleased never published unlocked units display 'Unpublished units will not be released' """ - self._verify_unit_warning( - self.UnitState(is_released=False, publish_state=self.PublishState.NEVER_PUBLISHED, is_locked=False), - self.NEVER_PUBLISHED_WARNING - ) - - def test_unreleased_unpublished_changes_locked(self): - """ Tests that unreleased unpublished changes locked units display staff only warnings """ - self._verify_unit_warning( - self.UnitState(is_released=False, publish_state=self.PublishState.UNPUBLISHED_CHANGES, is_locked=True), - self.STAFF_ONLY_WARNING - ) - - def test_unreleased_unpublished_changes_unlocked(self): - """ - Tests that unreleased unpublished changes unlocked units display 'Unpublished changes to content that will - release in the future' - """ - self._verify_unit_warning( - self.UnitState(is_released=False, publish_state=self.PublishState.UNPUBLISHED_CHANGES, is_locked=False), - self.FUTURE_UNPUBLISHED_WARNING - ) - - def test_unreleased_published_locked(self): - """ Tests that unreleased published locked units display staff only warnings """ - self._verify_unit_warning( - self.UnitState(is_released=False, publish_state=self.PublishState.PUBLISHED, is_locked=True), - self.STAFF_ONLY_WARNING - ) - - def test_unreleased_published_unlocked(self): - """ Tests that unreleased published unlocked units display no warnings """ - self._verify_unit_warning( - self.UnitState(is_released=False, publish_state=self.PublishState.PUBLISHED, is_locked=False), - None - ) - - def _verify_unit_warning(self, unit_state, expected_status_message): - """ - Verifies that the given unit's messages match the expected messages. - If expected_status_message is None, then the unit status message is expected to not be present. - """ - self._ensure_unit_present(unit_state) - self.course_outline_page.visit() - section = self.course_outline_page.section(unit_state.name) - subsection = section.subsection_at(0) - subsection.expand_subsection() - unit = subsection.unit_at(0) - if expected_status_message == self.STAFF_ONLY_WARNING: - self.assertEqual(section.status_message, self.STAFF_ONLY_WARNING) - self.assertEqual(subsection.status_message, self.STAFF_ONLY_WARNING) - self.assertEqual(unit.status_message, self.STAFF_ONLY_WARNING) - else: - self.assertFalse(section.has_status_message) - self.assertFalse(subsection.has_status_message) - if expected_status_message: - self.assertEqual(unit.status_message, expected_status_message) - else: - self.assertFalse(unit.has_status_message) - - def _ensure_unit_present(self, unit_state): - """ Ensures that a unit with the given state is present on the course outline """ - if unit_state.publish_state == self.PublishState.PUBLISHED: - return - - name = unit_state.name - self.course_outline_page.visit() - subsection = self.course_outline_page.section(name).subsection(name) - subsection.expand_subsection() - - if unit_state.publish_state == self.PublishState.UNPUBLISHED_CHANGES: - unit = subsection.unit(name).go_to() - add_discussion(unit) - elif unit_state.publish_state == self.PublishState.NEVER_PUBLISHED: - subsection.add_unit() - unit = ContainerPage(self.browser, None) - unit.wait_for_page() - unit.set_name(name) - - if unit.is_staff_locked != unit_state.is_locked: - unit.toggle_staff_lock() - - -@attr(shard=3) -class EditingSectionsTest(CourseOutlineTest): - """ - Feature: Editing Release date, Due date and grading type. - """ - - __test__ = True - - def test_can_edit_subsection(self): - """ - Scenario: I can edit settings of subsection. - - Given that I have created a subsection - Then I see release date, due date and grading policy of subsection in course outline - When I click on the configuration icon - Then edit modal window is shown - And release date, due date and grading policy fields present - And they have correct initial values - Then I set new values for these fields - And I click save button on the modal - Then I see release date, due date and grading policy of subsection in course outline - """ - self.course_outline_page.visit() - subsection = self.course_outline_page.section(SECTION_NAME).subsection(SUBSECTION_NAME) - - # Verify that Release date visible by default - self.assertTrue(subsection.release_date) - # Verify that Due date and Policy hidden by default - self.assertFalse(subsection.due_date) - self.assertFalse(subsection.policy) - - modal = subsection.edit() - - # Verify fields - self.assertTrue(modal.has_release_date()) - self.assertTrue(modal.has_release_time()) - self.assertTrue(modal.has_due_date()) - self.assertTrue(modal.has_due_time()) - self.assertTrue(modal.has_policy()) - - # Verify initial values - self.assertEqual(modal.release_date, u'1/1/1970') - self.assertEqual(modal.release_time, u'00:00') - self.assertEqual(modal.due_date, u'') - self.assertEqual(modal.due_time, u'') - self.assertEqual(modal.policy, u'Not Graded') - - # Set new values - modal.release_date = '3/12/1972' - modal.release_time = '04:01' - modal.due_date = '7/21/2014' - modal.due_time = '23:39' - modal.policy = 'Lab' - - modal.save() - self.assertIn(u'Released: Mar 12, 1972', subsection.release_date) - self.assertIn(u'04:01', subsection.release_date) - self.assertIn(u'Due: Jul 21, 2014', subsection.due_date) - self.assertIn(u'23:39', subsection.due_date) - self.assertIn(u'Lab', subsection.policy) - - def test_can_edit_section(self): - """ - Scenario: I can edit settings of section. - - Given that I have created a section - Then I see release date of section in course outline - When I click on the configuration icon - Then edit modal window is shown - And release date field present - And it has correct initial value - Then I set new value for this field - And I click save button on the modal - Then I see release date of section in course outline - """ - self.course_outline_page.visit() - section = self.course_outline_page.section(SECTION_NAME) - - # Verify that Release date visible by default - self.assertTrue(section.release_date) - # Verify that Due date and Policy are not present - self.assertFalse(section.due_date) - self.assertFalse(section.policy) - - modal = section.edit() - # Verify fields - self.assertTrue(modal.has_release_date()) - self.assertFalse(modal.has_due_date()) - self.assertFalse(modal.has_policy()) - - # Verify initial value - self.assertEqual(modal.release_date, u'1/1/1970') - - # Set new value - modal.release_date = '5/14/1969' - - modal.save() - self.assertIn(u'Released: May 14, 1969', section.release_date) - # Verify that Due date and Policy are not present - self.assertFalse(section.due_date) - self.assertFalse(section.policy) - - def test_subsection_is_graded_in_lms(self): - """ - Scenario: I can grade subsection from course outline page. - - Given I visit progress page - And I see that problem in subsection has grading type "Practice" - Then I visit course outline page - And I click on the configuration icon of subsection - And I set grading policy to "Lab" - And I click save button on the modal - Then I visit progress page - And I see that problem in subsection has grading type "Problem" - """ - progress_page = ProgressPage(self.browser, self.course_id) - progress_page.visit() - progress_page.wait_for_page() - self.assertEqual(u'Practice', progress_page.grading_formats[0]) - self.course_outline_page.visit() - - subsection = self.course_outline_page.section(SECTION_NAME).subsection(SUBSECTION_NAME) - modal = subsection.edit() - # Set new values - modal.policy = 'Lab' - modal.save() - - progress_page.visit() - - self.assertEqual(u'Problem', progress_page.grading_formats[0]) - - def test_unchanged_release_date_is_not_saved(self): - """ - Scenario: Saving a subsection without changing the release date will not override the release date - Given that I have created a section with a subsection - When I open the settings modal for the subsection - And I pressed save - And I open the settings modal for the section - And I change the release date to 07/20/1969 - And I press save - Then the subsection and the section have the release date 07/20/1969 - """ - self.course_outline_page.visit() - - modal = self.course_outline_page.section_at(0).subsection_at(0).edit() - modal.save() - - modal = self.course_outline_page.section_at(0).edit() - modal.release_date = '7/20/1969' - modal.save() - - release_text = 'Released: Jul 20, 1969' - self.assertIn(release_text, self.course_outline_page.section_at(0).release_date) - self.assertIn(release_text, self.course_outline_page.section_at(0).subsection_at(0).release_date) - - -@attr(shard=3) -class UnitAccessTest(CourseOutlineTest): - """ - Feature: Units can be restricted and unrestricted to certain groups from the course outline. - """ - - __test__ = True - - def setUp(self): - super(UnitAccessTest, self).setUp() - self.group_configurations_page = GroupConfigurationsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - self.content_group_a = "Test Group A" - self.content_group_b = "Test Group B" - - self.group_configurations_page.visit() - self.group_configurations_page.create_first_content_group() - config_a = self.group_configurations_page.content_groups[0] - config_a.name = self.content_group_a - config_a.save() - self.content_group_a_id = config_a.id - - self.group_configurations_page.add_content_group() - config_b = self.group_configurations_page.content_groups[1] - config_b.name = self.content_group_b - config_b.save() - self.content_group_b_id = config_b.id - - def populate_course_fixture(self, course_fixture): - """ - Create a course with one section, one subsection, and two units - """ - # with collapsed outline - self.chap_1_handle = 0 - self.chap_1_seq_1_handle = 1 - - # with first sequential expanded - self.seq_1_vert_1_handle = 2 - self.seq_1_vert_2_handle = 3 - self.chap_1_seq_2_handle = 4 - - course_fixture.add_children( - XBlockFixtureDesc('chapter', "1").add_children( - XBlockFixtureDesc('sequential', '1.1').add_children( - XBlockFixtureDesc('vertical', '1.1.1'), - XBlockFixtureDesc('vertical', '1.1.2') - ) - ) - ) - - -@attr(shard=14) -class StaffLockTest(CourseOutlineTest): - """ - Feature: Sections, subsections, and units can be locked and unlocked from the course outline. - """ - - __test__ = True - - def populate_course_fixture(self, course_fixture): - """ Create a course with one section, two subsections, and four units """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', '1').add_children( - XBlockFixtureDesc('sequential', '1.1').add_children( - XBlockFixtureDesc('vertical', '1.1.1'), - XBlockFixtureDesc('vertical', '1.1.2') - ), - XBlockFixtureDesc('sequential', '1.2').add_children( - XBlockFixtureDesc('vertical', '1.2.1'), - XBlockFixtureDesc('vertical', '1.2.2') - ) - ) - ) - - def _verify_descendants_are_staff_only(self, item): - """Verifies that all the descendants of item are staff only""" - self.assertTrue(item.is_staff_only) - if hasattr(item, 'children'): - for child in item.children(): - self._verify_descendants_are_staff_only(child) - - def _remove_staff_lock_and_verify_warning(self, outline_item, expect_warning): - """Removes staff lock from a course outline item and checks whether or not a warning appears.""" - modal = outline_item.edit() - modal.is_explicitly_locked = False - if expect_warning: - self.assertTrue(modal.shows_staff_lock_warning()) - else: - self.assertFalse(modal.shows_staff_lock_warning()) - modal.save() - - def _toggle_lock_on_unlocked_item(self, outline_item): - """Toggles outline_item's staff lock on and then off, verifying the staff lock warning""" - self.assertFalse(outline_item.has_staff_lock_warning) - outline_item.set_staff_lock(True) - self.assertTrue(outline_item.has_staff_lock_warning) - self._verify_descendants_are_staff_only(outline_item) - outline_item.set_staff_lock(False) - self.assertFalse(outline_item.has_staff_lock_warning) - - def _verify_explicit_staff_lock_remains_after_unlocking_parent(self, child_item, parent_item): - """Verifies that child_item's explicit staff lock remains after removing parent_item's staff lock""" - child_item.set_staff_lock(True) - parent_item.set_staff_lock(True) - self.assertTrue(parent_item.has_staff_lock_warning) - self.assertTrue(child_item.has_staff_lock_warning) - parent_item.set_staff_lock(False) - self.assertFalse(parent_item.has_staff_lock_warning) - self.assertTrue(child_item.has_staff_lock_warning) - - def test_units_can_be_locked(self): - """ - Scenario: Units can be locked and unlocked from the course outline page - Given I have a course with a unit - When I click on the configuration icon - And I enable explicit staff locking - And I click save - Then the unit shows a staff lock warning - And when I click on the configuration icon - And I disable explicit staff locking - And I click save - Then the unit does not show a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0) - self._toggle_lock_on_unlocked_item(unit) - - def test_subsections_can_be_locked(self): - """ - Scenario: Subsections can be locked and unlocked from the course outline page - Given I have a course with a subsection - When I click on the subsection's configuration icon - And I enable explicit staff locking - And I click save - Then the subsection shows a staff lock warning - And all its descendants are staff locked - And when I click on the subsection's configuration icon - And I disable explicit staff locking - And I click save - Then the the subsection does not show a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - subsection = self.course_outline_page.section_at(0).subsection_at(0) - self._toggle_lock_on_unlocked_item(subsection) - - def test_sections_can_be_locked(self): - """ - Scenario: Sections can be locked and unlocked from the course outline page - Given I have a course with a section - When I click on the section's configuration icon - And I enable explicit staff locking - And I click save - Then the section shows a staff lock warning - And all its descendants are staff locked - And when I click on the section's configuration icon - And I disable explicit staff locking - And I click save - Then the section does not show a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - section = self.course_outline_page.section_at(0) - self._toggle_lock_on_unlocked_item(section) - - def test_explicit_staff_lock_remains_after_unlocking_section(self): - """ - Scenario: An explicitly locked unit is still locked after removing an inherited lock from a section - Given I have a course with sections, subsections, and units - And I have enabled explicit staff lock on a section and one of its units - When I click on the section's configuration icon - And I disable explicit staff locking - And I click save - Then the unit still shows a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - section = self.course_outline_page.section_at(0) - unit = section.subsection_at(0).unit_at(0) - self._verify_explicit_staff_lock_remains_after_unlocking_parent(unit, section) - - def test_explicit_staff_lock_remains_after_unlocking_subsection(self): - """ - Scenario: An explicitly locked unit is still locked after removing an inherited lock from a subsection - Given I have a course with sections, subsections, and units - And I have enabled explicit staff lock on a subsection and one of its units - When I click on the subsection's configuration icon - And I disable explicit staff locking - And I click save - Then the unit still shows a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - subsection = self.course_outline_page.section_at(0).subsection_at(0) - unit = subsection.unit_at(0) - self._verify_explicit_staff_lock_remains_after_unlocking_parent(unit, subsection) - - def test_section_displays_lock_when_all_subsections_locked(self): - """ - Scenario: All subsections in section are explicitly locked, section should display staff only warning - Given I have a course one section and two subsections - When I enable explicit staff lock on all the subsections - Then the section shows a staff lock warning - """ - self.course_outline_page.visit() - section = self.course_outline_page.section_at(0) - section.subsection_at(0).set_staff_lock(True) - section.subsection_at(1).set_staff_lock(True) - self.assertTrue(section.has_staff_lock_warning) - - def test_section_displays_lock_when_all_units_locked(self): - """ - Scenario: All units in a section are explicitly locked, section should display staff only warning - Given I have a course with one section, two subsections, and four units - When I enable explicit staff lock on all the units - Then the section shows a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - section = self.course_outline_page.section_at(0) - section.subsection_at(0).unit_at(0).set_staff_lock(True) - section.subsection_at(0).unit_at(1).set_staff_lock(True) - section.subsection_at(1).unit_at(0).set_staff_lock(True) - section.subsection_at(1).unit_at(1).set_staff_lock(True) - self.assertTrue(section.has_staff_lock_warning) - - def test_subsection_displays_lock_when_all_units_locked(self): - """ - Scenario: All units in subsection are explicitly locked, subsection should display staff only warning - Given I have a course with one subsection and two units - When I enable explicit staff lock on all the units - Then the subsection shows a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - subsection = self.course_outline_page.section_at(0).subsection_at(0) - subsection.unit_at(0).set_staff_lock(True) - subsection.unit_at(1).set_staff_lock(True) - self.assertTrue(subsection.has_staff_lock_warning) - - def test_section_does_not_display_lock_when_some_subsections_locked(self): - """ - Scenario: Only some subsections in section are explicitly locked, section should NOT display staff only warning - Given I have a course with one section and two subsections - When I enable explicit staff lock on one subsection - Then the section does not show a staff lock warning - """ - self.course_outline_page.visit() - section = self.course_outline_page.section_at(0) - section.subsection_at(0).set_staff_lock(True) - self.assertFalse(section.has_staff_lock_warning) - - def test_section_does_not_display_lock_when_some_units_locked(self): - """ - Scenario: Only some units in section are explicitly locked, section should NOT display staff only warning - Given I have a course with one section, two subsections, and four units - When I enable explicit staff lock on three units - Then the section does not show a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - section = self.course_outline_page.section_at(0) - section.subsection_at(0).unit_at(0).set_staff_lock(True) - section.subsection_at(0).unit_at(1).set_staff_lock(True) - section.subsection_at(1).unit_at(1).set_staff_lock(True) - self.assertFalse(section.has_staff_lock_warning) - - def test_subsection_does_not_display_lock_when_some_units_locked(self): - """ - Scenario: Only some units in subsection are explicitly locked, subsection should NOT display staff only warning - Given I have a course with one subsection and two units - When I enable explicit staff lock on one unit - Then the subsection does not show a staff lock warning - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - subsection = self.course_outline_page.section_at(0).subsection_at(0) - subsection.unit_at(0).set_staff_lock(True) - self.assertFalse(subsection.has_staff_lock_warning) - - def test_locked_sections_do_not_appear_in_lms(self): - """ - Scenario: A locked section is not visible to students in the LMS - Given I have a course with two sections - When I enable explicit staff lock on one section - And I click the View Live button to switch to staff view - And I visit the course home with the outline - Then I see two sections in the outline - And when I switch the view mode to student view - Then I see one section in the outline - """ - self.course_outline_page.visit() - self.course_outline_page.add_section_from_top_button() - self.course_outline_page.section_at(1).set_staff_lock(True) - self.course_outline_page.view_live() - - course_home_page = CourseHomePage(self.browser, self.course_id) - course_home_page.visit() - course_home_page.wait_for_page() - self.assertEqual(course_home_page.outline.num_sections, 2) - course_home_page.preview.set_staff_view_mode('Learner') - course_home_page.wait_for(lambda: course_home_page.outline.num_sections == 1, - 'Only 1 section is visible in the outline') - - def test_toggling_staff_lock_on_section_does_not_publish_draft_units(self): - """ - Scenario: Locking and unlocking a section will not publish its draft units - Given I have a course with a section and unit - And the unit has a draft and published version - When I enable explicit staff lock on the section - And I disable explicit staff lock on the section - And I click the View Live button to switch to staff view - Then I see the published version of the unit - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to() - add_discussion(unit) - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - section = self.course_outline_page.section_at(0) - section.set_staff_lock(True) - section.set_staff_lock(False) - unit = section.subsection_at(0).unit_at(0).go_to() - unit.view_published_version() - courseware = CoursewarePage(self.browser, self.course_id) - courseware.wait_for_page() - self.assertEqual(courseware.num_xblock_components, 0) - - def test_toggling_staff_lock_on_subsection_does_not_publish_draft_units(self): - """ - Scenario: Locking and unlocking a subsection will not publish its draft units - Given I have a course with a subsection and unit - And the unit has a draft and published version - When I enable explicit staff lock on the subsection - And I disable explicit staff lock on the subsection - And I click the View Live button to switch to staff view - Then I see the published version of the unit - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to() - add_discussion(unit) - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - subsection = self.course_outline_page.section_at(0).subsection_at(0) - subsection.set_staff_lock(True) - subsection.set_staff_lock(False) - unit = subsection.unit_at(0).go_to() - unit.view_published_version() - courseware = CoursewarePage(self.browser, self.course_id) - courseware.wait_for_page() - self.assertEqual(courseware.num_xblock_components, 0) - - def test_removing_staff_lock_from_unit_without_inherited_lock_shows_warning(self): - """ - Scenario: Removing explicit staff lock from a unit which does not inherit staff lock displays a warning. - Given I have a course with a subsection and unit - When I enable explicit staff lock on the unit - And I disable explicit staff lock on the unit - Then I see a modal warning. - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0) - unit.set_staff_lock(True) - self._remove_staff_lock_and_verify_warning(unit, True) - - def test_removing_staff_lock_from_subsection_without_inherited_lock_shows_warning(self): - """ - Scenario: Removing explicit staff lock from a subsection which does not inherit staff lock displays a warning. - Given I have a course with a section and subsection - When I enable explicit staff lock on the subsection - And I disable explicit staff lock on the subsection - Then I see a modal warning. - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - subsection = self.course_outline_page.section_at(0).subsection_at(0) - subsection.set_staff_lock(True) - self._remove_staff_lock_and_verify_warning(subsection, True) - - def test_removing_staff_lock_from_unit_with_inherited_lock_shows_no_warning(self): - """ - Scenario: Removing explicit staff lock from a unit which also inherits staff lock displays no warning. - Given I have a course with a subsection and unit - When I enable explicit staff lock on the subsection - And I enable explicit staff lock on the unit - When I disable explicit staff lock on the unit - Then I do not see a modal warning. - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - subsection = self.course_outline_page.section_at(0).subsection_at(0) - unit = subsection.unit_at(0) - subsection.set_staff_lock(True) - unit.set_staff_lock(True) - self._remove_staff_lock_and_verify_warning(unit, False) - - def test_removing_staff_lock_from_subsection_with_inherited_lock_shows_no_warning(self): - """ - Scenario: Removing explicit staff lock from a subsection which also inherits staff lock displays no warning. - Given I have a course with a section and subsection - When I enable explicit staff lock on the section - And I enable explicit staff lock on the subsection - When I disable explicit staff lock on the subsection - Then I do not see a modal warning. - """ - self.course_outline_page.visit() - self.course_outline_page.expand_all_subsections() - section = self.course_outline_page.section_at(0) - subsection = section.subsection_at(0) - section.set_staff_lock(True) - subsection.set_staff_lock(True) - self._remove_staff_lock_and_verify_warning(subsection, False) - - -@attr(shard=20) -class EditNamesTest(CourseOutlineTest): - """ - Feature: Click-to-edit section/subsection names - """ - - __test__ = True - - def set_name_and_verify(self, item, old_name, new_name, expected_name): - """ - Changes the display name of item from old_name to new_name, then verifies that its value is expected_name. - """ - self.assertEqual(item.name, old_name) - item.change_name(new_name) - self.assertFalse(item.in_editable_form()) - self.assertEqual(item.name, expected_name) - - def test_edit_section_name(self): - """ - Scenario: Click-to-edit section name - Given that I have created a section - When I click on the name of section - Then the section name becomes editable - And given that I have edited the section name - When I click outside of the edited section name - Then the section name saves - And becomes non-editable - """ - self.course_outline_page.visit() - self.set_name_and_verify( - self.course_outline_page.section_at(0), - 'Test Section', - 'Changed', - 'Changed' - ) - - def test_edit_subsection_name(self): - """ - Scenario: Click-to-edit subsection name - Given that I have created a subsection - When I click on the name of subsection - Then the subsection name becomes editable - And given that I have edited the subsection name - When I click outside of the edited subsection name - Then the subsection name saves - And becomes non-editable - """ - self.course_outline_page.visit() - self.set_name_and_verify( - self.course_outline_page.section_at(0).subsection_at(0), - 'Test Subsection', - 'Changed', - 'Changed' - ) - - def test_edit_empty_section_name(self): - """ - Scenario: Click-to-edit section name, enter empty name - Given that I have created a section - And I have clicked to edit the name of the section - And I have entered an empty section name - When I click outside of the edited section name - Then the section name does not change - And becomes non-editable - """ - self.course_outline_page.visit() - self.set_name_and_verify( - self.course_outline_page.section_at(0), - 'Test Section', - '', - 'Test Section' - ) - - def test_edit_empty_subsection_name(self): - """ - Scenario: Click-to-edit subsection name, enter empty name - Given that I have created a subsection - And I have clicked to edit the name of the subsection - And I have entered an empty subsection name - When I click outside of the edited subsection name - Then the subsection name does not change - And becomes non-editable - """ - self.course_outline_page.visit() - self.set_name_and_verify( - self.course_outline_page.section_at(0).subsection_at(0), - 'Test Subsection', - '', - 'Test Subsection' - ) - - def test_editing_names_does_not_expand_collapse(self): - """ - Scenario: A section stays in the same expand/collapse state while its name is edited - Given that I have created a section - And the section is collapsed - When I click on the name of the section - Then the section is collapsed - And given that I have entered a new name - Then the section is collapsed - And given that I press ENTER to finalize the name - Then the section is collapsed - """ - self.course_outline_page.visit() - self.course_outline_page.section_at(0).expand_subsection() - self.assertFalse(self.course_outline_page.section_at(0).in_editable_form()) - self.assertTrue(self.course_outline_page.section_at(0).is_collapsed) - self.course_outline_page.section_at(0).edit_name() - self.assertTrue(self.course_outline_page.section_at(0).in_editable_form()) - self.assertTrue(self.course_outline_page.section_at(0).is_collapsed) - self.course_outline_page.section_at(0).enter_name('Changed') - self.assertTrue(self.course_outline_page.section_at(0).is_collapsed) - self.course_outline_page.section_at(0).finalize_name() - self.assertTrue(self.course_outline_page.section_at(0).is_collapsed) - - -@attr(shard=14) -class CreateSectionsTest(CourseOutlineTest): - """ - Feature: Create new sections/subsections/units - """ - - __test__ = True - - def populate_course_fixture(self, course_fixture): - """ Start with a completely empty course to easily test adding things to it """ - pass - - def test_create_new_section_from_top_button(self): - """ - Scenario: Create new section from button at top of page - Given that I am on the course outline - When I click the "+ Add section" button at the top of the page - Then I see a new section added to the bottom of the page - And the display name is in its editable form. - """ - self.course_outline_page.visit() - self.course_outline_page.add_section_from_top_button() - self.assertEqual(len(self.course_outline_page.sections()), 1) - self.assertTrue(self.course_outline_page.section_at(0).in_editable_form()) - - def test_create_new_section_from_bottom_button(self): - """ - Scenario: Create new section from button at bottom of page - Given that I am on the course outline - When I click the "+ Add section" button at the bottom of the page - Then I see a new section added to the bottom of the page - And the display name is in its editable form. - """ - self.course_outline_page.visit() - self.course_outline_page.add_section_from_bottom_button() - self.assertEqual(len(self.course_outline_page.sections()), 1) - self.assertTrue(self.course_outline_page.section_at(0).in_editable_form()) - - def test_create_new_section_from_bottom_button_plus_icon(self): - """ - Scenario: Create new section from button plus icon at bottom of page - Given that I am on the course outline - When I click the plus icon in "+ Add section" button at the bottom of the page - Then I see a new section added to the bottom of the page - And the display name is in its editable form. - """ - self.course_outline_page.visit() - self.course_outline_page.add_section_from_bottom_button(click_child_icon=True) - self.assertEqual(len(self.course_outline_page.sections()), 1) - self.assertTrue(self.course_outline_page.section_at(0).in_editable_form()) - - def test_create_new_subsection(self): - """ - Scenario: Create new subsection - Given that I have created a section - When I click the "+ Add subsection" button in that section - Then I see a new subsection added to the bottom of the section - And the display name is in its editable form. - """ - self.course_outline_page.visit() - self.course_outline_page.add_section_from_top_button() - self.assertEqual(len(self.course_outline_page.sections()), 1) - self.course_outline_page.section_at(0).add_subsection() - subsections = self.course_outline_page.section_at(0).subsections() - self.assertEqual(len(subsections), 1) - self.assertTrue(subsections[0].in_editable_form()) - - def test_create_new_unit(self): - """ - Scenario: Create new unit - Given that I have created a section - And that I have created a subsection within that section - When I click the "+ Add unit" button in that subsection - Then I am redirected to a New Unit page - And the display name is in its editable form. - """ - self.course_outline_page.visit() - self.course_outline_page.add_section_from_top_button() - self.assertEqual(len(self.course_outline_page.sections()), 1) - self.course_outline_page.section_at(0).add_subsection() - self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) - self.course_outline_page.section_at(0).subsection_at(0).add_unit() - unit_page = ContainerPage(self.browser, None) - unit_page.wait_for_page() - self.assertTrue(unit_page.is_inline_editing_display_name()) - - -@attr(shard=4) -class DeleteContentTest(CourseOutlineTest): - """ - Feature: Deleting sections/subsections/units - """ - - __test__ = True - - def test_delete_section(self): - """ - Scenario: Delete section - Given that I am on the course outline - When I click the delete button for a section on the course outline - Then I should receive a confirmation message, asking me if I really want to delete the section - When I click "Yes, I want to delete this component" - Then the confirmation message should close - And the section should immediately be deleted from the course outline - """ - self.course_outline_page.visit() - self.assertEqual(len(self.course_outline_page.sections()), 1) - self.course_outline_page.section_at(0).delete() - self.assertEqual(len(self.course_outline_page.sections()), 0) - - def test_cancel_delete_section(self): - """ - Scenario: Cancel delete of section - Given that I clicked the delte button for a section on the course outline - And I received a confirmation message, asking me if I really want to delete the component - When I click "Cancel" - Then the confirmation message should close - And the section should remain in the course outline - """ - self.course_outline_page.visit() - self.assertEqual(len(self.course_outline_page.sections()), 1) - self.course_outline_page.section_at(0).delete(cancel=True) - self.assertEqual(len(self.course_outline_page.sections()), 1) - - def test_delete_subsection(self): - """ - Scenario: Delete subsection - Given that I am on the course outline - When I click the delete button for a subsection on the course outline - Then I should receive a confirmation message, asking me if I really want to delete the subsection - When I click "Yes, I want to delete this component" - Then the confiramtion message should close - And the subsection should immediately be deleted from the course outline - """ - self.course_outline_page.visit() - self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) - self.course_outline_page.section_at(0).subsection_at(0).delete() - self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 0) - - def test_cancel_delete_subsection(self): - """ - Scenario: Cancel delete of subsection - Given that I clicked the delete button for a subsection on the course outline - And I received a confirmation message, asking me if I really want to delete the subsection - When I click "cancel" - Then the confirmation message should close - And the subsection should remain in the course outline - """ - self.course_outline_page.visit() - self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) - self.course_outline_page.section_at(0).subsection_at(0).delete(cancel=True) - self.assertEqual(len(self.course_outline_page.section_at(0).subsections()), 1) - - def test_delete_unit(self): - """ - Scenario: Delete unit - Given that I am on the course outline - When I click the delete button for a unit on the course outline - Then I should receive a confirmation message, asking me if I really want to delete the unit - When I click "Yes, I want to delete this unit" - Then the confirmation message should close - And the unit should immediately be deleted from the course outline - """ - self.course_outline_page.visit() - self.course_outline_page.section_at(0).subsection_at(0).expand_subsection() - self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 1) - self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).delete() - self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 0) - - def test_cancel_delete_unit(self): - """ - Scenario: Cancel delete of unit - Given that I clicked the delete button for a unit on the course outline - And I received a confirmation message, asking me if I really want to delete the unit - When I click "Cancel" - Then the confirmation message should close - And the unit should remain in the course outline - """ - self.course_outline_page.visit() - self.course_outline_page.section_at(0).subsection_at(0).expand_subsection() - self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 1) - self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).delete(cancel=True) - self.assertEqual(len(self.course_outline_page.section_at(0).subsection_at(0).units()), 1) - - def test_delete_all_no_content_message(self): - """ - Scenario: Delete all sections/subsections/units in a course, "no content" message should appear - Given that I delete all sections, subsections, and units in a course - When I visit the course outline - Then I will see a message that says, "You haven't added any content to this course yet" - Add see a + Add Section button - """ - self.course_outline_page.visit() - self.assertFalse(self.course_outline_page.has_no_content_message) - self.course_outline_page.section_at(0).delete() - self.assertEqual(len(self.course_outline_page.sections()), 0) - self.assertTrue(self.course_outline_page.has_no_content_message) - - -@attr(shard=14) -class ExpandCollapseMultipleSectionsTest(CourseOutlineTest): - """ - Feature: Courses with multiple sections can expand and collapse all sections. - """ - - __test__ = True - - def populate_course_fixture(self, course_fixture): - """ Start with a course with two sections """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ), - XBlockFixtureDesc('chapter', 'Test Section 2').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection 2').add_children( - XBlockFixtureDesc('vertical', 'Test Unit 2') - ) - ) - ) - - def verify_all_sections(self, collapsed): - """ - Verifies that all sections are collapsed if collapsed is True, otherwise all expanded. - """ - for section in self.course_outline_page.sections(): - self.assertEqual(collapsed, section.is_collapsed) - - def toggle_all_sections(self): - """ - Toggles the expand collapse state of all sections. - """ - for section in self.course_outline_page.sections(): - section.expand_subsection() - - def test_expanded_by_default(self): - """ - Scenario: The default layout for the outline page is to show sections in expanded view - Given I have a course with sections - When I navigate to the course outline page - Then I see the "Collapse All Sections" link - And all sections are expanded - """ - self.course_outline_page.visit() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) - self.verify_all_sections(collapsed=False) - - def test_no_expand_link_for_empty_course(self): - """ - Scenario: Collapse link is removed after last section of a course is deleted - Given I have a course with multiple sections - And I navigate to the course outline page - When I will confirm all alerts - And I press the "section" delete icon - Then I do not see the "Collapse All Sections" link - And I will see a message that says "You haven't added any content to this course yet" - """ - self.course_outline_page.visit() - for section in self.course_outline_page.sections(): - section.delete() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) - self.assertTrue(self.course_outline_page.has_no_content_message) - - def test_collapse_all_when_all_expanded(self): - """ - Scenario: Collapse all sections when all sections are expanded - Given I navigate to the outline page of a course with sections - And all sections are expanded - When I click the "Collapse All Sections" link - Then I see the "Expand All Sections" link - And all sections are collapsed - """ - self.course_outline_page.visit() - self.verify_all_sections(collapsed=False) - self.course_outline_page.toggle_expand_collapse() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) - self.verify_all_sections(collapsed=True) - - def test_collapse_all_when_some_expanded(self): - """ - Scenario: Collapsing all sections when 1 or more sections are already collapsed - Given I navigate to the outline page of a course with sections - And all sections are expanded - When I collapse the first section - And I click the "Collapse All Sections" link - Then I see the "Expand All Sections" link - And all sections are collapsed - """ - self.course_outline_page.visit() - self.verify_all_sections(collapsed=False) - self.course_outline_page.section_at(0).expand_subsection() - self.course_outline_page.toggle_expand_collapse() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) - self.verify_all_sections(collapsed=True) - - def test_expand_all_when_all_collapsed(self): - """ - Scenario: Expanding all sections when all sections are collapsed - Given I navigate to the outline page of a course with multiple sections - And I click the "Collapse All Sections" link - When I click the "Expand All Sections" link - Then I see the "Collapse All Sections" link - And all sections are expanded - """ - self.course_outline_page.visit() - self.course_outline_page.toggle_expand_collapse() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) - self.course_outline_page.toggle_expand_collapse() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) - self.verify_all_sections(collapsed=False) - - def test_expand_all_when_some_collapsed(self): - """ - Scenario: Expanding all sections when 1 or more sections are already expanded - Given I navigate to the outline page of a course with multiple sections - And I click the "Collapse All Sections" link - When I expand the first section - And I click the "Expand All Sections" link - Then I see the "Collapse All Sections" link - And all sections are expanded - """ - self.course_outline_page.visit() - # We have seen unexplainable sporadic failures in this test. Try disabling animations to see - # if that helps. - disable_animations(self.course_outline_page) - self.course_outline_page.toggle_expand_collapse() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.EXPAND) - self.verify_all_sections(collapsed=True) - self.course_outline_page.section_at(0).expand_subsection() - self.course_outline_page.toggle_expand_collapse() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) - self.verify_all_sections(collapsed=False) - - -@attr(shard=14) -class ExpandCollapseSingleSectionTest(CourseOutlineTest): - """ - Feature: Courses with a single section can expand and collapse all sections. - """ - - __test__ = True - - def test_no_expand_link_for_empty_course(self): - """ - Scenario: Collapse link is removed after last section of a course is deleted - Given I have a course with one section - And I navigate to the course outline page - When I will confirm all alerts - And I press the "section" delete icon - Then I do not see the "Collapse All Sections" link - And I will see a message that says "You haven't added any content to this course yet" - """ - self.course_outline_page.visit() - self.course_outline_page.section_at(0).delete() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) - self.assertTrue(self.course_outline_page.has_no_content_message) - - def test_old_subsection_stays_collapsed_after_creation(self): - """ - Scenario: Collapsed subsection stays collapsed after creating a new subsection - Given I have a course with one section and subsection - And I navigate to the course outline page - Then the subsection is collapsed - And when I create a new subsection - Then the first subsection is collapsed - And the second subsection is expanded - """ - self.course_outline_page.visit() - self.assertTrue(self.course_outline_page.section_at(0).subsection_at(0).is_collapsed) - self.course_outline_page.section_at(0).add_subsection() - self.assertTrue(self.course_outline_page.section_at(0).subsection_at(0).is_collapsed) - self.assertFalse(self.course_outline_page.section_at(0).subsection_at(1).is_collapsed) - - -@attr(shard=14) -class ExpandCollapseEmptyTest(CourseOutlineTest): - """ - Feature: Courses with no sections initially can expand and collapse all sections after addition. - """ - - __test__ = True - - def populate_course_fixture(self, course_fixture): - """ Start with an empty course """ - pass - - def test_no_expand_link_for_empty_course(self): - """ - Scenario: Expand/collapse for a course with no sections - Given I have a course with no sections - When I navigate to the course outline page - Then I do not see the "Collapse All Sections" link - """ - self.course_outline_page.visit() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) - - def test_link_appears_after_section_creation(self): - """ - Scenario: Collapse link appears after creating first section of a course - Given I have a course with no sections - When I navigate to the course outline page - And I add a section - Then I see the "Collapse All Sections" link - And all sections are expanded - """ - self.course_outline_page.visit() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.MISSING) - self.course_outline_page.add_section_from_top_button() - self.assertEqual(self.course_outline_page.expand_collapse_link_state, ExpandCollapseLinkState.COLLAPSE) - self.assertFalse(self.course_outline_page.section_at(0).is_collapsed) - - -@attr(shard=20) -class DefaultStatesEmptyTest(CourseOutlineTest): - """ - Feature: Misc course outline default states/actions when starting with an empty course - """ - - __test__ = True - - def populate_course_fixture(self, course_fixture): - """ Start with an empty course """ - pass - - def test_empty_course_message(self): - """ - Scenario: Empty course state - Given that I am in a course with no sections, subsections, nor units - When I visit the course outline - Then I will see a message that says "You haven't added any content to this course yet" - And see a + Add Section button - """ - self.course_outline_page.visit() - self.assertTrue(self.course_outline_page.has_no_content_message) - self.assertTrue(self.course_outline_page.bottom_add_section_button.is_present()) - - -@attr(shard=4) -class DefaultStatesContentTest(CourseOutlineTest): - """ - Feature: Misc course outline default states/actions when starting with a course with content - """ - - __test__ = True - - def test_view_live(self): - """ - Scenario: View Live version from course outline - Given that I am on the course outline - When I click the "View Live" button - Then a new tab will open to the course on the LMS - """ - self.course_outline_page.visit() - self.course_outline_page.view_live() - courseware = CoursewarePage(self.browser, self.course_id) - courseware.wait_for_page() - self.assertEqual(courseware.num_xblock_components, 3) - self.assertEqual(courseware.xblock_component_type(0), 'problem') - self.assertEqual(courseware.xblock_component_type(1), 'html') - self.assertEqual(courseware.xblock_component_type(2), 'discussion') - - -@attr(shard=7) -class UnitNavigationTest(CourseOutlineTest): - """ - Feature: Navigate to units - """ - - __test__ = True - - def test_navigate_to_unit(self): - """ - Scenario: Click unit name to navigate to unit page - Given that I have expanded a section/subsection so I can see unit names - When I click on a unit name - Then I will be taken to the appropriate unit page - """ - self.course_outline_page.visit() - self.course_outline_page.section_at(0).subsection_at(0).expand_subsection() - unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to() - unit.wait_for_page() - - -@attr(shard=7) -class PublishSectionTest(CourseOutlineTest): - """ - Feature: Publish sections. - """ - - __test__ = True - - def populate_course_fixture(self, course_fixture): - """ - Sets up a course structure with 2 subsections inside a single section. - The first subsection has 2 units, and the second subsection has one unit. - """ - self.courseware = CoursewarePage(self.browser, self.course_id) - self.course_home_page = CourseHomePage(self.browser, self.course_id) - course_fixture.add_children( - XBlockFixtureDesc('chapter', SECTION_NAME).add_children( - XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children( - XBlockFixtureDesc('vertical', UNIT_NAME), - XBlockFixtureDesc('vertical', 'Test Unit 2'), - ), - XBlockFixtureDesc('sequential', 'Test Subsection 2').add_children( - XBlockFixtureDesc('vertical', 'Test Unit 3'), - ), - ), - ) - - def test_unit_publishing(self): - """ - Scenario: Can publish a unit and see published content in LMS - Given I have a section with 2 subsections and 3 unpublished units - When I go to the course outline - Then I see publish button for the first unit, subsection, section - When I publish the first unit - Then I see that publish button for the first unit disappears - And I see publish buttons for subsection, section - And I see the changed content in LMS - """ - self._add_unpublished_content() - self.course_outline_page.visit() - section, subsection, unit = self._get_items() - self.assertTrue(unit.publish_action) - self.assertTrue(subsection.publish_action) - self.assertTrue(section.publish_action) - unit.publish() - self.assertFalse(unit.publish_action) - self.assertTrue(subsection.publish_action) - self.assertTrue(section.publish_action) - self.courseware.visit() - self.assertEqual(1, self.courseware.num_xblock_components) - - def test_subsection_publishing(self): - """ - Scenario: Can publish a subsection and see published content in LMS - Given I have a section with 2 subsections and 3 unpublished units - When I go to the course outline - Then I see publish button for the unit, subsection, section - When I publish the first subsection - Then I see that publish button for the first subsection disappears - And I see that publish buttons disappear for the child units of the subsection - And I see publish button for section - And I see the changed content in LMS - """ - self._add_unpublished_content() - self.course_outline_page.visit() - section, subsection, unit = self._get_items() - self.assertTrue(unit.publish_action) - self.assertTrue(subsection.publish_action) - self.assertTrue(section.publish_action) - self.course_outline_page.section(SECTION_NAME).subsection(SUBSECTION_NAME).publish() - self.assertFalse(unit.publish_action) - self.assertFalse(subsection.publish_action) - self.assertTrue(section.publish_action) - self.courseware.visit() - self.assertEqual(1, self.courseware.num_xblock_components) - self.courseware.go_to_sequential_position(2) - self.assertEqual(1, self.courseware.num_xblock_components) - - def test_section_publishing(self): - """ - Scenario: Can publish a section and see published content in LMS - Given I have a section with 2 subsections and 3 unpublished units - When I go to the course outline - Then I see publish button for the unit, subsection, section - When I publish the section - Then I see that publish buttons disappears - And I see the changed content in LMS - """ - self._add_unpublished_content() - self.course_outline_page.visit() - section, subsection, unit = self._get_items() - self.assertTrue(subsection.publish_action) - self.assertTrue(section.publish_action) - self.assertTrue(unit.publish_action) - self.course_outline_page.section(SECTION_NAME).publish() - self.assertFalse(subsection.publish_action) - self.assertFalse(section.publish_action) - self.assertFalse(unit.publish_action) - self.courseware.visit() - self.assertEqual(1, self.courseware.num_xblock_components) - self.courseware.go_to_sequential_position(2) - self.assertEqual(1, self.courseware.num_xblock_components) - self.course_home_page.visit() - self.course_home_page.outline.go_to_section(SECTION_NAME, 'Test Subsection 2') - self.assertEqual(1, self.courseware.num_xblock_components) - - def _add_unpublished_content(self): - """ - Adds unpublished HTML content to first three units in the course. - """ - for index in range(3): - self.course_fixture.create_xblock( - self.course_fixture.get_nested_xblocks(category="vertical")[index].locator, - XBlockFixtureDesc('html', 'Unpublished HTML Component ' + str(index)), - ) - - def _get_items(self): - """ - Returns first section, subsection, and unit on the page. - """ - section = self.course_outline_page.section(SECTION_NAME) - subsection = section.subsection(SUBSECTION_NAME) - unit = subsection.expand_subsection().unit(UNIT_NAME) - - return (section, subsection, unit) - - -@attr(shard=7) -class DeprecationWarningMessageTest(CourseOutlineTest): - """ - Feature: Verify deprecation warning message. - """ - HEADING_TEXT = 'This course uses features that are no longer supported.' - COMPONENT_LIST_HEADING = 'You must delete or replace the following components.' - ADVANCE_MODULES_REMOVE_TEXT = ( - u'To avoid errors, édX strongly recommends that you remove unsupported features ' - u'from the course advanced settings. To do this, go to the Advanced Settings ' - u'page, locate the "Advanced Module List" setting, and then delete the following ' - u'modules from the list.' - ) - DEFAULT_DISPLAYNAME = "Deprecated Component" - - def _add_deprecated_advance_modules(self, block_types): - """ - Add `block_types` into `Advanced Module List` - - Arguments: - block_types (list): list of block types - """ - self.advanced_settings.visit() - self.advanced_settings.set_values({"Advanced Module List": json.dumps(block_types)}) - - def _create_deprecated_components(self): - """ - Create deprecated components. - """ - parent_vertical = self.course_fixture.get_nested_xblocks(category="vertical")[0] - - self.course_fixture.create_xblock( - parent_vertical.locator, - XBlockFixtureDesc('poll', "Poll", data=load_data_str('poll_markdown.xml')) - ) - self.course_fixture.create_xblock(parent_vertical.locator, XBlockFixtureDesc('survey', 'Survey')) - - def _verify_deprecation_warning_info( - self, - deprecated_blocks_present, - components_present, - components_display_name_list=None, - deprecated_modules_list=None - ): - """ - Verify deprecation warning - - Arguments: - deprecated_blocks_present (bool): deprecated blocks remove text and - is list is visible if True else False - components_present (bool): components list shown if True else False - components_display_name_list (list): list of components display name - deprecated_modules_list (list): list of deprecated advance modules - """ - self.assertTrue(self.course_outline_page.deprecated_warning_visible) - self.assertEqual(self.course_outline_page.warning_heading_text, self.HEADING_TEXT) - self.assertEqual(self.course_outline_page.modules_remove_text_shown, deprecated_blocks_present) - if deprecated_blocks_present: - self.assertEqual(self.course_outline_page.modules_remove_text, self.ADVANCE_MODULES_REMOVE_TEXT) - self.assertEqual(self.course_outline_page.deprecated_advance_modules, deprecated_modules_list) - - self.assertEqual(self.course_outline_page.components_visible, components_present) - if components_present: - self.assertEqual(self.course_outline_page.components_list_heading, self.COMPONENT_LIST_HEADING) - six.assertCountEqual(self, self.course_outline_page.components_display_names, components_display_name_list) - - def test_no_deprecation_warning_message_present(self): - """ - Scenario: Verify that deprecation warning message is not shown if no deprecated - advance modules are not present and also no deprecated component exist in - course outline. - - When I goto course outline - Then I don't see any deprecation warning - """ - self.course_outline_page.visit() - self.assertFalse(self.course_outline_page.deprecated_warning_visible) - - def test_deprecation_warning_message_present(self): - """ - Scenario: Verify deprecation warning message if deprecated modules - and components are present. - - Given I have "poll" advance modules present in `Advanced Module List` - And I have created 2 poll components - When I go to course outline - Then I see poll deprecated warning - And I see correct poll deprecated warning heading text - And I see correct poll deprecated warning advance modules remove text - And I see list of poll components with correct display names - """ - self._add_deprecated_advance_modules(block_types=['poll', 'survey']) - self._create_deprecated_components() - self.course_outline_page.visit() - self._verify_deprecation_warning_info( - deprecated_blocks_present=True, - components_present=True, - components_display_name_list=['Poll', 'Survey'], - deprecated_modules_list=['poll', 'survey'] - ) - - def test_deprecation_warning_with_no_displayname(self): - """ - Scenario: Verify deprecation warning message if poll components are present. - - Given I have created 1 poll deprecated component - When I go to course outline - Then I see poll deprecated warning - And I see correct poll deprecated warning heading text - And I see list of poll components with correct message - """ - parent_vertical = self.course_fixture.get_nested_xblocks(category="vertical")[0] - - # Create a deprecated component with display_name to be empty and make sure - # the deprecation warning is displayed with - self.course_fixture.create_xblock( - parent_vertical.locator, - XBlockFixtureDesc(category='poll', display_name="", data=load_data_str('poll_markdown.xml')) - ) - self.course_outline_page.visit() - - self._verify_deprecation_warning_info( - deprecated_blocks_present=False, - components_present=True, - components_display_name_list=[self.DEFAULT_DISPLAYNAME], - ) - - def test_warning_with_poll_advance_modules_only(self): - """ - Scenario: Verify that deprecation warning message is shown if only - poll advance modules are present and no poll component exist. - - Given I have poll advance modules present in `Advanced Module List` - When I go to course outline - Then I see poll deprecated warning - And I see correct poll deprecated warning heading text - And I see correct poll deprecated warning advance modules remove text - And I don't see list of poll components - """ - self._add_deprecated_advance_modules(block_types=['poll', 'survey']) - self.course_outline_page.visit() - self._verify_deprecation_warning_info( - deprecated_blocks_present=True, - components_present=False, - deprecated_modules_list=['poll', 'survey'] - ) - - def test_warning_with_poll_components_only(self): - """ - Scenario: Verify that deprecation warning message is shown if only - poll component exist and no poll advance modules are present. - - Given I have created two poll components - When I go to course outline - Then I see poll deprecated warning - And I see correct poll deprecated warning heading text - And I don't see poll deprecated warning advance modules remove text - And I see list of poll components with correct display names - """ - self._create_deprecated_components() - self.course_outline_page.visit() - self._verify_deprecation_warning_info( - deprecated_blocks_present=False, - components_present=True, - components_display_name_list=['Poll', 'Survey'] - ) - - -@attr(shard=4) -class SelfPacedOutlineTest(CourseOutlineTest): - """Test the course outline for a self-paced course.""" - - def populate_course_fixture(self, course_fixture): - course_fixture.add_children( - XBlockFixtureDesc('chapter', SECTION_NAME).add_children( - XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children( - XBlockFixtureDesc('vertical', UNIT_NAME) - ) - ), - ) - self.course_fixture.add_course_details({ - 'self_paced': True, - 'start_date': datetime.now() + timedelta(days=1) - }) - ConfigModelFixture('/config/self_paced', {'enabled': True}).install() - - def test_release_dates_not_shown(self): - """ - Scenario: Ensure that block release dates are not shown on the - course outline page of a self-paced course. - - Given I am the author of a self-paced course - When I go to the course outline - Then I should not see release dates for course content - """ - self.course_outline_page.visit() - section = self.course_outline_page.section(SECTION_NAME) - self.assertEqual(section.release_date, '') - subsection = section.subsection(SUBSECTION_NAME) - self.assertEqual(subsection.release_date, '') - - def test_edit_section_and_subsection(self): - """ - Scenario: Ensure that block release/due dates are not shown - in their settings modals. - - Given I am the author of a self-paced course - When I go to the course outline - And I click on settings for a section or subsection - Then I should not see release or due date settings - """ - self.course_outline_page.visit() - section = self.course_outline_page.section(SECTION_NAME) - modal = section.edit() - self.assertFalse(modal.has_release_date()) - self.assertFalse(modal.has_due_date()) - modal.cancel() - subsection = section.subsection(SUBSECTION_NAME) - modal = subsection.edit() - self.assertFalse(modal.has_release_date()) - self.assertFalse(modal.has_due_date()) - - -class CourseStatusOutlineTest(CourseOutlineTest): - """Test the course outline status section.""" - shard = 8 - - def setUp(self): - super(CourseStatusOutlineTest, self).setUp() - - self.schedule_and_details_settings = SettingsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self.checklists = CourseChecklistsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - def test_course_status_section(self): - """ - Ensure that the course status section appears in the course outline. - """ - self.course_outline_page.visit() - self.assertTrue(self.course_outline_page.has_course_status_section) - - def test_course_status_section_start_date_link(self): - """ - Ensure that the course start date link in the course status section in - the course outline links to the "Schedule and Details" page. - """ - self.course_outline_page.visit() - self.course_outline_page.click_course_status_section_start_date_link() - self.schedule_and_details_settings.wait_for_page() - - def test_course_status_section_checklists_link(self): - """ - Ensure that the course checklists link in the course status section in - the course outline links to the "Checklists" page. - """ - self.course_outline_page.visit() - self.course_outline_page.click_course_status_section_checklists_link() - self.checklists.wait_for_page() - - -class InstructorPacedToSelfPacedOutlineTest(CourseOutlineTest): - """ - Test the course outline when pacing is changed from - instructor to self paced. - """ - def populate_course_fixture(self, course_fixture): - course_fixture.add_children( - XBlockFixtureDesc('chapter', SECTION_NAME).add_children( - XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children( - XBlockFixtureDesc('vertical', UNIT_NAME) - ) - ), - ) - self.course_fixture.add_course_details({ - 'start_date': datetime.now() + timedelta(days=1), - }) - self.course_fixture.add_advanced_settings({ - 'enable_timed_exams': { - 'value': True - } - }) - - def test_due_dates_not_shown(self): - """ - Scenario: Ensure that due dates for timed exams - are not displayed on the course outline page when switched to - self-paced mode from instructor-paced. - - Given an instructor paced course, add a due date for a subsection. - Change the course's pacing to self-paced. - Make the subsection a timed exam. - Make sure adding the timed exam doesn't display the due date. - """ - self.course_outline_page.visit() - section = self.course_outline_page.section(SECTION_NAME) - subsection = section.subsection(SUBSECTION_NAME) - - modal = subsection.edit() - modal.due_date = '5/14/2016' - modal.policy = 'Homework' - modal.save() - # Checking if the added due date saved - self.assertIn('May 14', subsection.due_date) - # Checking if grading policy added - self.assertEqual('Homework', subsection.policy) - - # Updating the course mode to self-paced - self.course_fixture.add_course_details({ - 'self_paced': True - }) - # Making the subsection a timed exam - self.course_outline_page.open_subsection_settings_dialog() - self.course_outline_page.select_advanced_tab() - self.course_outline_page.make_exam_timed() - - # configure call to actually update course with new settings - self.course_fixture.configure_course() - # Reloading page after the changes - self.course_outline_page.visit() - self.assertIsNone(subsection.due_date) diff --git a/common/test/acceptance/tests/studio/test_studio_problem_editor.py b/common/test/acceptance/tests/studio/test_studio_problem_editor.py deleted file mode 100644 index 4b0b036efe..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_problem_editor.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Acceptance tests for Problem component in studio -""" - - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.studio.container import ContainerPage -from common.test.acceptance.pages.studio.problem_editor import ProblemXBlockEditorView -from common.test.acceptance.pages.studio.utils import add_component -from common.test.acceptance.tests.helpers import skip_if_browser -from common.test.acceptance.tests.studio.base_studio_test import ContainerBase - - -class ProblemComponentEditor(ContainerBase): - """ - Feature: CMS.Component Adding - As a course author, I want to be able to add and edit Problem - """ - - def setUp(self, is_staff=True): - """ - Create a course with a section, subsection, and unit to which to add the component. - """ - super(ProblemComponentEditor, self).setUp(is_staff=is_staff) - self.component = 'Blank Common Problem' - self.unit = self.go_to_unit_page() - self.container_page = ContainerPage(self.browser, None) - # Add a Problem - add_component(self.container_page, 'problem', self.component) - self.component = self.unit.xblocks[1] - self.container_page.edit() - self.problem_editor = ProblemXBlockEditorView(self.browser, self.component.locator) - - def populate_course_fixture(self, course_fixture): - """ - Adds a course fixture - """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ) - ) - - def test_user_can_modify_float_input(self): - """ - Scenario: User can modify float input values - Given I have created a Blank Common Problem - When I edit and select Settings - Then I can set the weight to "3.5" - And my change to weight is persisted - And I can revert to the default value of unset for weight - """ - self.problem_editor.open_settings() - self.problem_editor.set_field_val('Problem Weight', '3.5') - self.problem_editor.save() - - # reopen settings - self.container_page.edit() - self.problem_editor.open_settings() - - field_value = self.problem_editor.get_field_val('Problem Weight') - self.assertEqual(field_value, '3.5') - self.problem_editor.revert_setting() - field_value = self.problem_editor.get_field_val('Problem Weight') - self.assertEqual(field_value, '', 'Component settings is not reverted to default') - - @skip_if_browser('firefox') - # Lettuce tests run on chrome and chrome does not allow to enter - # periods/dots in this field and consequently we have to save the - # value as '234'. Whereas, bokchoy runs with the older version of - # firefox on jenkins, which does not allow to save the value if it - # has a period/dot. Clicking on save button after filling '2.34' in - # field, does not do anything and test does not go any further. - # So, it fails always. - def test_user_cannot_type_decimal_values(self): - """ - Scenario: User cannot type decimal values integer number field - Given I have created a Blank Common Problem - When I edit and select Settings - Then if I set the max attempts to "2.34", it will persist as a valid integer - """ - self.problem_editor.open_settings() - self.problem_editor.set_field_val('Maximum Attempts', '2.34') - self.problem_editor.save() - - # reopen settings - self.container_page.edit() - self.problem_editor.open_settings() - - field_value = self.problem_editor.get_field_val('Maximum Attempts') - self.assertEqual(field_value, '234', "Decimal values are not allowed in this field") - - def test_settings_are_not_saved_on_cancel(self): - """ - Scenario: Settings changes are not saved on Cancel - Given I have created a Blank Common Problem - When I edit and select Settings - Then I can set the weight to "3.5" - And I can modify the display name - Then If I press Cancel my changes are not persisted - """ - self.problem_editor.open_settings() - self.problem_editor.set_field_val('Problem Weight', '3.5') - self.problem_editor.cancel() - - # reopen settings - self.container_page.edit() - self.problem_editor.open_settings() - - field_value = self.problem_editor.get_field_val('Problem Weight') - self.assertEqual(field_value, '', "Component setting should not appear updated if cancelled during editing") - - def test_cheat_sheet_visible_on_toggle(self): - """ - Scenario: Cheat sheet visible on toggle - Given I have created a Blank Common Problem - And I can edit the problem - Then I can see cheatsheet - """ - self.problem_editor.toggle_cheatsheet() - self.assertTrue(self.problem_editor.is_cheatsheet_present(), "Cheatsheet not present") diff --git a/common/test/acceptance/tests/studio/test_studio_settings.py b/common/test/acceptance/tests/studio/test_studio_settings.py index d28aae2ea8..03ce60acf3 100644 --- a/common/test/acceptance/tests/studio/test_studio_settings.py +++ b/common/test/acceptance/tests/studio/test_studio_settings.py @@ -5,230 +5,13 @@ Acceptance tests for Studio's Setting pages import os -from textwrap import dedent - -from bok_choy.promise import EmptyPromise from mock import patch from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.common.utils import add_enrollment_course_modes from common.test.acceptance.pages.studio.overview import CourseOutlinePage from common.test.acceptance.pages.studio.settings import SettingsPage -from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage -from common.test.acceptance.pages.studio.utils import get_input_value -from common.test.acceptance.tests.helpers import create_user_partition_json, element_has_text from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest from openedx.core.lib.tests import attr -from xmodule.partitions.partitions import Group - - -@attr(shard=19) -class ContentGroupConfigurationTest(StudioCourseTest): - """ - Tests for content groups in the Group Configurations Page. - There are tests for the experiment groups in test_studio_split_test. - """ - def setUp(self): - super(ContentGroupConfigurationTest, self).setUp() - self.group_configurations_page = GroupConfigurationsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self.outline_page = CourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - def populate_course_fixture(self, course_fixture): - """ - Populates test course with chapter, sequential, and 1 problems. - The problem is visible only to Group "alpha". - """ - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit') - ) - ) - ) - - def create_and_verify_content_group(self, name, existing_groups): - """ - Creates a new content group and verifies that it was properly created. - """ - self.assertEqual(existing_groups, len(self.group_configurations_page.content_groups)) - if existing_groups == 0: - self.group_configurations_page.create_first_content_group() - else: - self.group_configurations_page.add_content_group() - config = self.group_configurations_page.content_groups[existing_groups] - config.name = name - # Save the content group - self.assertEqual(config.get_text('.action-primary'), "Create") - self.assertFalse(config.delete_button_is_present) - config.save() - self.assertIn(name, config.name) - return config - - def test_no_content_groups_by_default(self): - """ - Scenario: Ensure that message telling me to create a new content group is - shown when no content groups exist. - Given I have a course without content groups - When I go to the Group Configuration page in Studio - Then I see "You have not created any content groups yet." message - """ - self.group_configurations_page.visit() - self.assertTrue(self.group_configurations_page.no_content_groups_message_is_present) - self.assertIn( - "You have not created any content groups yet.", - self.group_configurations_page.no_content_groups_message_text - ) - - def test_can_create_and_edit_content_groups(self): - """ - Scenario: Ensure that the content groups can be created and edited correctly. - Given I have a course without content groups - When I click button 'Add your first Content Group' - And I set new the name and click the button 'Create' - Then I see the new content is added and has correct data - And I click 'New Content Group' button - And I set the name and click the button 'Create' - Then I see the second content group is added and has correct data - When I edit the second content group - And I change the name and click the button 'Save' - Then I see the second content group is saved successfully and has the new name - """ - self.group_configurations_page.visit() - self.create_and_verify_content_group("New Content Group", 0) - second_config = self.create_and_verify_content_group("Second Content Group", 1) - - # Edit the second content group - second_config.edit() - second_config.name = "Updated Second Content Group" - self.assertEqual(second_config.get_text('.action-primary'), "Save") - second_config.save() - - self.assertIn("Updated Second Content Group", second_config.name) - - def test_cannot_delete_used_content_group(self): - """ - Scenario: Ensure that the user cannot delete used content group. - Given I have a course with 1 Content Group - And I go to the Group Configuration page - When I try to delete the Content Group with name "New Content Group" - Then I see the delete button is disabled. - """ - self.course_fixture._update_xblock(self.course_fixture._course_location, { - "metadata": { - u"user_partitions": [ - create_user_partition_json( - 0, - 'Configuration alpha,', - 'Content Group Partition', - [Group("0", 'alpha')], - scheme="cohort" - ) - ], - }, - }) - problem_data = dedent(""" - -

          Choose Yes.

          - - - Yes - - -
          - """) - vertical = self.course_fixture.get_nested_xblocks(category="vertical")[0] - self.course_fixture.create_xblock( - vertical.locator, - XBlockFixtureDesc('problem', "VISIBLE TO ALPHA", data=problem_data, metadata={"group_access": {0: [0]}}), - ) - self.group_configurations_page.visit() - config = self.group_configurations_page.content_groups[0] - self.assertTrue(config.delete_button_is_disabled) - - def test_can_delete_unused_content_group(self): - """ - Scenario: Ensure that the user can delete unused content group. - Given I have a course with 1 Content Group - And I go to the Group Configuration page - When I delete the Content Group with name "New Content Group" - Then I see that there is no Content Group - When I refresh the page - Then I see that the content group has been deleted - """ - self.group_configurations_page.visit() - config = self.create_and_verify_content_group("New Content Group", 0) - self.assertTrue(config.delete_button_is_present) - - self.assertEqual(len(self.group_configurations_page.content_groups), 1) - - # Delete content group - config.delete() - self.assertEqual(len(self.group_configurations_page.content_groups), 0) - - self.group_configurations_page.visit() - self.assertEqual(len(self.group_configurations_page.content_groups), 0) - - def test_must_supply_name(self): - """ - Scenario: Ensure that validation of the content group works correctly. - Given I have a course without content groups - And I create new content group without specifying a name click the button 'Create' - Then I see error message "Content Group name is required." - When I set a name and click the button 'Create' - Then I see the content group is saved successfully - """ - self.group_configurations_page.visit() - self.group_configurations_page.create_first_content_group() - config = self.group_configurations_page.content_groups[0] - config.save() - self.assertEqual(config.mode, 'edit') - self.assertEqual("Group name is required", config.validation_message) - config.name = "Content Group Name" - config.save() - self.assertIn("Content Group Name", config.name) - - def test_can_cancel_creation_of_content_group(self): - """ - Scenario: Ensure that creation of a content group can be canceled correctly. - Given I have a course without content groups - When I click button 'Add your first Content Group' - And I set new the name and click the button 'Cancel' - Then I see that there is no content groups in the course - """ - self.group_configurations_page.visit() - self.group_configurations_page.create_first_content_group() - config = self.group_configurations_page.content_groups[0] - config.name = "Content Group" - config.cancel() - self.assertEqual(0, len(self.group_configurations_page.content_groups)) - - def test_content_group_empty_usage(self): - """ - Scenario: When content group is not used, ensure that the link to outline page works correctly. - Given I have a course without content group - And I create new content group - Then I see a link to the outline page - When I click on the outline link - Then I see the outline page - """ - self.group_configurations_page.visit() - config = self.create_and_verify_content_group("New Content Group", 0) - config.toggle() - config.click_outline_anchor() - - # Waiting for the page load and verify that we've landed on course outline page - self.outline_page.wait_for_page() @attr('a11y') @@ -326,344 +109,3 @@ class StudioSubsectionSettingsA11yTest(StudioCourseTest): include=['section.edit-settings-timed-examination'] ) self.course_outline.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=15) -class StudioSettingsImageUploadTest(StudioCourseTest): - """ - Class to test course settings image uploads. - """ - def setUp(self): # pylint: disable=arguments-differ - super(StudioSettingsImageUploadTest, self).setUp() - self.settings_page = SettingsPage(self.browser, self.course_info['org'], self.course_info['number'], - self.course_info['run']) - self.settings_page.visit() - - # Ensure jquery is loaded before running a jQuery - self.settings_page.wait_for_ajax() - # This text appears towards the end of the work that jQuery is performing on the page - self.settings_page.wait_for_jquery_value('input#course-name:text', 'test_run') - - def test_upload_course_card_image(self): - - # upload image - file_to_upload = 'image.jpg' - self.settings_page.upload_image('#upload-course-image', file_to_upload) - self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#course-image')) - - def test_upload_course_banner_image(self): - - # upload image - file_to_upload = 'image.jpg' - self.settings_page.upload_image('#upload-banner-image', file_to_upload) - self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#banner-image')) - - def test_upload_course_video_thumbnail_image(self): - - # upload image - file_to_upload = 'image.jpg' - self.settings_page.upload_image('#upload-video-thumbnail-image', file_to_upload) - self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#video-thumbnail-image')) - - -@attr(shard=16) -class CourseSettingsTest(StudioCourseTest): - """ - Class to test course settings. - """ - COURSE_START_DATE_CSS = "#course-start-date" - COURSE_END_DATE_CSS = "#course-end-date" - ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date" - ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date" - - COURSE_START_TIME_CSS = "#course-start-time" - COURSE_END_TIME_CSS = "#course-end-time" - ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time" - ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time" - - course_start_date = '12/20/2013' - course_end_date = '12/26/2013' - enrollment_start_date = '12/01/2013' - enrollment_end_date = '12/10/2013' - - dummy_time = "15:30" - - def setUp(self, is_staff=False, test_xss=True): - super(CourseSettingsTest, self).setUp() - - self.settings_page = SettingsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # Before every test, make sure to visit the page first - self.settings_page.visit() - self.ensure_input_fields_are_loaded() - - def set_course_dates(self): - """ - Set dates for the course. - """ - dates_dictionary = { - self.COURSE_START_DATE_CSS: self.course_start_date, - self.COURSE_END_DATE_CSS: self.course_end_date, - self.ENROLLMENT_START_DATE_CSS: self.enrollment_start_date, - self.ENROLLMENT_END_DATE_CSS: self.enrollment_end_date - } - - self.settings_page.set_element_values(dates_dictionary) - - def ensure_input_fields_are_loaded(self): - """ - Ensures values in input fields are loaded. - """ - EmptyPromise( - lambda: self.settings_page.q(css='#course-organization').attrs('value')[0], - "Waiting for input fields to be loaded" - ).fulfill() - - def test_user_can_set_course_date(self): - """ - Scenario: User can set course dates - Given I have opened a new course in Studio - When I select Schedule and Details - And I set course dates - And I press the "Save" notification button - And I reload the page - Then I see the set dates - """ - - # Set dates - self.set_course_dates() - # Set times - time_dictionary = { - self.COURSE_START_TIME_CSS: self.dummy_time, - self.ENROLLMENT_END_TIME_CSS: self.dummy_time - } - self.settings_page.set_element_values(time_dictionary) - # Save changes - self.settings_page.save_changes() - self.settings_page.refresh_and_wait_for_load() - self.ensure_input_fields_are_loaded() - css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS, - self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS, - self.COURSE_START_TIME_CSS, self.ENROLLMENT_END_TIME_CSS] - - expected_values = [self.course_start_date, self.course_end_date, - self.enrollment_start_date, self.enrollment_end_date, - self.dummy_time, self.dummy_time] - # Assert changes have been persistent. - self.assertEqual( - [get_input_value(self.settings_page, css_selector) for css_selector in css_selectors], - expected_values - ) - - def test_clear_previously_set_course_dates(self): - """ - Scenario: User can clear previously set course dates (except start date) - Given I have set course dates - And I clear all the dates except start - And I press the "Save" notification button - And I reload the page - Then I see cleared dates - """ - - # Set dates - self.set_course_dates() - # Clear all dates except start date - values_to_set = { - self.COURSE_END_DATE_CSS: '', - self.ENROLLMENT_START_DATE_CSS: '', - self.ENROLLMENT_END_DATE_CSS: '' - } - self.settings_page.set_element_values(values_to_set) - # Save changes and refresh the page - self.settings_page.save_changes() - self.settings_page.refresh_and_wait_for_load() - self.ensure_input_fields_are_loaded() - css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS, - self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS] - - expected_values = [self.course_start_date, '', '', ''] - # Assert changes have been persistent. - self.assertEqual( - [get_input_value(self.settings_page, css_selector) for css_selector in css_selectors], - expected_values - ) - - def test_cannot_clear_the_course_start_date(self): - """ - Scenario: User cannot clear the course start date - Given I have set course dates - And I press the "Save" notification button - And I clear the course start date - Then I receive a warning about course start date - And I reload the page - And the previously set start date is shown - """ - # Set dates - self.set_course_dates() - # Save changes - self.settings_page.save_changes() - # Get default start date - default_start_date = get_input_value(self.settings_page, self.COURSE_START_DATE_CSS) - # Set course start date to empty - self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: ''}) - # Make sure error message is show with appropriate message - error_message_css = '.message-error' - self.settings_page.wait_for_element_presence(error_message_css, 'Error message is present') - self.assertEqual(element_has_text(self.settings_page, error_message_css, - "The course must have an assigned start date."), True) - # Refresh the page and assert start date has not changed. - self.settings_page.refresh_and_wait_for_load() - self.ensure_input_fields_are_loaded() - self.assertEqual( - get_input_value(self.settings_page, self.COURSE_START_DATE_CSS), - default_start_date - ) - - def test_user_can_correct_course_start_date_warning(self): - """ - Scenario: User can correct the course start date warning - Given I have tried to clear the course start - And I have entered a new course start date - And I press the "Save" notification button - Then The warning about course start date goes away - And I reload the page - Then my new course start date is shown - """ - # Set course start date to empty - self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: ''}) - # Make sure we get error message - error_message_css = '.message-error' - self.settings_page.wait_for_element_presence(error_message_css, 'Error message is present') - self.assertEqual(element_has_text(self.settings_page, error_message_css, - "The course must have an assigned start date."), True) - # Set new course start value - self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: self.course_start_date}) - self.settings_page.un_focus_input_field() - # Error message disappears - self.settings_page.wait_for_element_absence(error_message_css, 'Error message is not present') - # Save the changes and refresh the page. - self.settings_page.save_changes() - self.settings_page.refresh_and_wait_for_load() - self.ensure_input_fields_are_loaded() - # Assert changes are persistent. - self.assertEqual( - get_input_value(self.settings_page, self.COURSE_START_DATE_CSS), - self.course_start_date - ) - - def test_settings_are_only_persisted_when_saved(self): - """ - Scenario: Settings are only persisted when saved - Given I have set course dates - And I press the "Save" notification button - When I change fields - And I reload the page - Then I do not see the changes - """ - # Set course dates. - self.set_course_dates() - # Save changes. - self.settings_page.save_changes() - default_value_enrollment_start_date = get_input_value(self.settings_page, - self.ENROLLMENT_START_TIME_CSS) - # Set the value of enrollment start time and - # reload the page without saving. - self.settings_page.set_element_values({self.ENROLLMENT_START_TIME_CSS: self.dummy_time}) - self.settings_page.refresh_and_wait_for_load() - self.ensure_input_fields_are_loaded() - - css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS, - self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS, - self.ENROLLMENT_START_TIME_CSS] - - expected_values = [self.course_start_date, self.course_end_date, - self.enrollment_start_date, self.enrollment_end_date, - default_value_enrollment_start_date] - # Assert that value of enrolment start time - # is not saved. - self.assertEqual( - [get_input_value(self.settings_page, css_selector) for css_selector in css_selectors], - expected_values - ) - - def test_settings_are_reset_on_cancel(self): - """ - Scenario: Settings are reset on cancel - Given I have set course dates - And I press the "Save" notification button - When I change fields - And I press the "Cancel" notification button - Then I do not see the changes - """ - # Set course date - self.set_course_dates() - # Save changes - self.settings_page.save_changes() - default_value_enrollment_start_date = get_input_value(self.settings_page, - self.ENROLLMENT_START_TIME_CSS) - # Set value but don't save it. - self.settings_page.set_element_values({self.ENROLLMENT_START_TIME_CSS: self.dummy_time}) - self.settings_page.click_button("cancel") - # Make sure changes are not saved after cancel. - css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS, - self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS, - self.ENROLLMENT_START_TIME_CSS] - - expected_values = [self.course_start_date, self.course_end_date, - self.enrollment_start_date, self.enrollment_end_date, - default_value_enrollment_start_date] - - self.assertEqual( - [get_input_value(self.settings_page, css_selector) for css_selector in css_selectors], - expected_values - ) - - def test_confirmation_is_shown_on_save(self): - """ - Scenario: Confirmation is shown on save - Given I have opened a new course in Studio - When I select Schedule and Details - And I change the "" field to "" - And I press the "Save" notification button - Then I see a confirmation that my changes have been saved - """ - # Set date - self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: self.course_start_date}) - # Confirmation is showed upon save. - # Save_changes function ensures that save - # confirmation is shown. - self.settings_page.save_changes() - - def test_changes_in_course_overview_show_a_confirmation(self): - """ - Scenario: Changes in Course Overview show a confirmation - Given I have opened a new course in Studio - When I select Schedule and Details - And I change the course overview - And I press the "Save" notification button - Then I see a confirmation that my changes have been saved - """ - # Change the value of course overview - self.settings_page.change_course_description('Changed overview') - # Save changes - # Save_changes function ensures that save - # confirmation is shown. - self.settings_page.save_changes() - - def test_user_cannot_save_invalid_settings(self): - """ - Scenario: User cannot save invalid settings - Given I have opened a new course in Studio - When I select Schedule and Details - And I change the "Course Start Date" field to "" - Then the save notification button is disabled - """ - # Change the course start date to invalid date. - self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: ''}) - # Confirm that save button is disabled. - self.assertEqual(self.settings_page.is_element_present(".action-primary.action-save.is-disabled"), True) diff --git a/common/test/acceptance/tests/studio/test_studio_settings_certificates.py b/common/test/acceptance/tests/studio/test_studio_settings_certificates.py deleted file mode 100644 index 2e8e7a82a5..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_settings_certificates.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -Acceptance tests for Studio's Setting pages -""" - - -import re - -from common.test.acceptance.pages.lms.create_mode import ModeCreationPage -from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettingsPage -from common.test.acceptance.pages.studio.settings_certificates import CertificatesPage -from common.test.acceptance.tests.helpers import skip_if_browser -from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest - - -class CertificatesTest(StudioCourseTest): - """ - Tests for settings/certificates Page. - """ - shard = 22 - - def setUp(self): # pylint: disable=arguments-differ - super(CertificatesTest, self).setUp(is_staff=True, test_xss=False) - self.certificates_page = CertificatesPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - self.advanced_settings_page = AdvancedSettingsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - self.course_advanced_settings = dict() - - # Add a verified mode to the course - ModeCreationPage( - self.browser, self.course_id, mode_slug=u'verified', mode_display_name=u'Verified Certificate', - min_price=10, suggested_prices='10,20' - ).visit() - - def make_signatory_data(self, prefix='First'): - """ - Makes signatory dict which can be used in the tests to create certificates - """ - return { - 'name': u'{prefix} Signatory Name'.format(prefix=prefix), - 'title': u'{prefix} Signatory Title'.format(prefix=prefix), - 'organization': u'{prefix} Signatory Organization'.format(prefix=prefix), - } - - def create_and_verify_certificate(self, course_title_override, existing_certs, signatories): - """ - Creates a new certificate and verifies that it was properly created. - """ - self.assertEqual(existing_certs, len(self.certificates_page.certificates)) - if existing_certs == 0: - self.certificates_page.wait_for_first_certificate_button() - self.certificates_page.click_first_certificate_button() - else: - self.certificates_page.wait_for_add_certificate_button() - self.certificates_page.click_add_certificate_button() - - certificate = self.certificates_page.certificates[existing_certs] - - # Set the certificate properties - certificate.course_title = course_title_override - - # add signatories - added_signatories = 0 - for idx, signatory in enumerate(signatories): - certificate.signatories[idx].name = signatory['name'] - certificate.signatories[idx].title = signatory['title'] - certificate.signatories[idx].organization = signatory['organization'] - certificate.signatories[idx].upload_signature_image('Signature-{}.png'.format(idx)) - - added_signatories += 1 - if len(signatories) > added_signatories: - certificate.click_add_signatory_button() - - # Save the certificate - self.assertEqual(certificate.get_text('.action-primary'), "Create") - certificate.click_create_certificate_button() - self.assertIn(course_title_override, certificate.course_title) - return certificate - - def test_no_certificates_by_default(self): - """ - Scenario: Ensure that message telling me to create a new certificate is - shown when no certificate exist. - Given I have a course without certificates - When I go to the Certificates page in Studio - Then I see "You have not created any certificates yet." message and - a link with text "Set up your certificate" - """ - self.certificates_page.visit() - self.assertTrue(self.certificates_page.no_certificates_message_shown) - self.assertIn( - "You have not created any certificates yet.", - self.certificates_page.no_certificates_message_text - ) - self.assertIn( - "Set up your certificate", - self.certificates_page.new_certificate_link_text - ) - - def test_can_create_and_edit_certficate(self): - """ - Scenario: Ensure that the certificates can be created and edited correctly. - Given I have a course without certificates - When I click button 'Add your first Certificate' - And I set new the course title override and signatory and click the button 'Create' - Then I see the new certificate is added and has correct data - When I edit the certificate - And I change the name and click the button 'Save' - Then I see the certificate is saved successfully and has the new name - """ - self.certificates_page.visit() - self.certificates_page.wait_for_first_certificate_button() - certificate = self.create_and_verify_certificate( - "Course Title Override", - 0, - [self.make_signatory_data('first'), self.make_signatory_data('second')] - ) - - # Edit the certificate - certificate.click_edit_certificate_button() - certificate.course_title = "Updated Course Title Override 2" - self.assertEqual(certificate.get_text('.action-primary'), "Save") - certificate.click_save_certificate_button() - - self.assertIn("Updated Course Title Override 2", certificate.course_title) - - def test_can_delete_certificate(self): - """ - Scenario: Ensure that the user can delete certificate. - Given I have a course with 1 certificate - And I go to the Certificates page - When I delete the Certificate with name "New Certificate" - Then I see that there is no certificate - When I refresh the page - Then I see that the certificate has been deleted - """ - self.certificates_page.visit() - certificate = self.create_and_verify_certificate( - "Course Title Override", - 0, - [self.make_signatory_data('first'), self.make_signatory_data('second')] - ) - - certificate.wait_for_certificate_delete_button() - - self.assertEqual(len(self.certificates_page.certificates), 1) - - # Delete the certificate we just created - certificate.click_delete_certificate_button() - self.certificates_page.click_confirmation_prompt_primary_button() - - # Reload the page and confirm there are no certificates - self.certificates_page.visit() - self.assertEqual(len(self.certificates_page.certificates), 0) - - @skip_if_browser('chrome') # TODO Need to fix this for chrome browser - def test_can_create_and_edit_signatories_of_certficate(self): - """ - Scenario: Ensure that the certificates can be created with signatories and edited correctly. - Given I have a course without certificates - When I click button 'Add your first Certificate' - And I set new the course title override and signatory and click the button 'Create' - Then I see the new certificate is added and has one signatory inside it - When I click 'Edit' button of signatory panel - And I set the name and click the button 'Save' icon - Then I see the signatory name updated with newly set name - When I refresh the certificates page - Then I can see course has one certificate with new signatory name - When I click 'Edit' button of signatory panel - And click on 'Close' button - Then I can see no change in signatory detail - """ - self.certificates_page.visit() - certificate = self.create_and_verify_certificate( - "Course Title Override", - 0, - [self.make_signatory_data('first')] - ) - self.assertEqual(len(self.certificates_page.certificates), 1) - # Edit the signatory in certificate - signatory = certificate.signatories[0] - signatory.edit() - - signatory.name = 'Updated signatory name' - signatory.title = 'Update signatory title' - signatory.organization = 'Updated signatory organization' - signatory.save() - - self.assertEqual(len(self.certificates_page.certificates), 1) - - #Refreshing the page, So page have the updated certificate object. - self.certificates_page.refresh() - self.certificates_page.wait_for_page() - signatory = self.certificates_page.certificates[0].signatories[0] - self.assertIn("Updated signatory name", signatory.name) - self.assertIn("Update signatory title", signatory.title) - self.assertIn("Updated signatory organization", signatory.organization) - - signatory.edit() - signatory.close() - - self.assertIn("Updated signatory name", signatory.name) - - def test_can_cancel_creation_of_certificate(self): - """ - Scenario: Ensure that creation of a certificate can be canceled correctly. - Given I have a course without certificates - When I click button 'Add your first Certificate' - And I set name of certificate and click the button 'Cancel' - Then I see that there is no certificates in the course - """ - self.certificates_page.visit() - self.certificates_page.click_first_certificate_button() - certificate = self.certificates_page.certificates[0] - certificate.course_title = "Title Override" - certificate.click_cancel_edit_certificate() - self.assertEqual(len(self.certificates_page.certificates), 0) - - def test_line_breaks_in_signatory_title(self): - """ - Scenario: Ensure that line breaks are properly reflected in certificate - - Given I have a certificate with signatories - When I add signatory title with new line character - Then I see line break in certificate title - """ - self.certificates_page.visit() - certificate = self.create_and_verify_certificate( - "Course Title Override", - 0, - [ - { - 'name': 'Signatory Name', - 'title': 'Signatory title with new line character \n', - 'organization': 'Signatory Organization', - } - ] - ) - - certificate.wait_for_certificate_delete_button() - - # Make sure certificate is created - self.assertEqual(len(self.certificates_page.certificates), 1) - - signatory_title = self.certificates_page.get_first_signatory_title() - self.assertNotEqual([], re.findall(r'', signatory_title)) - - def test_course_number_in_certificate_details_view(self): - """ - Scenario: Ensure that Course Number is displayed in certificate details view - - Given I have a certificate - When I visit certificate details page on studio - Then I see Course Number next to Course Name - """ - self.certificates_page.visit() - certificate = self.create_and_verify_certificate( - "Course Title Override", - 0, - [self.make_signatory_data('first')] - ) - - certificate.wait_for_certificate_delete_button() - - # Make sure certificate is created - self.assertEqual(len(self.certificates_page.certificates), 1) - course_number = self.certificates_page.get_course_number() - self.assertEqual(self.course_info['number'], course_number) - - def test_course_number_override_in_certificate_details_view(self): - """ - Scenario: Ensure that Course Number Override is displayed in certificate details view - - Given I have a certificate - When I visit certificate details page on studio then course number override should be hidden. - Then I visit the course advance settings page and set the value for course override number. - Then I see Course Number Override next to Course Name in certificate settings page. - """ - - self.course_advanced_settings.update( - {'Course Number Display String': 'Course Number Override String'} - ) - - self.certificates_page.visit() - certificate = self.create_and_verify_certificate( - "Course Title Override", - 0, - [self.make_signatory_data('first')] - ) - self.assertFalse(self.certificates_page.course_number_override().present) - certificate.wait_for_certificate_delete_button() - - # Make sure certificate is created - self.assertEqual(len(self.certificates_page.certificates), 1) - - # set up course number override in Advanced Settings Page - self.advanced_settings_page.visit() - self.advanced_settings_page.set_values(self.course_advanced_settings) - self.advanced_settings_page.wait_for_ajax() - - self.certificates_page.visit() - course_number_override = self.certificates_page.get_course_number_override() - self.assertEqual(self.course_advanced_settings['Course Number Display String'], course_number_override) - self.assertTrue(self.certificates_page.course_number_override().present) diff --git a/common/test/acceptance/tests/studio/test_studio_settings_details.py b/common/test/acceptance/tests/studio/test_studio_settings_details.py deleted file mode 100644 index e67ee481da..0000000000 --- a/common/test/acceptance/tests/studio/test_studio_settings_details.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Acceptance tests for Studio's Settings Details pages -""" - - -from datetime import datetime, timedelta - -import six - -from common.test.acceptance.fixtures.config import ConfigModelFixture -from common.test.acceptance.fixtures.course import CourseFixture -from common.test.acceptance.pages.studio.overview import CourseOutlinePage -from common.test.acceptance.pages.studio.settings import SettingsPage -from common.test.acceptance.tests.helpers import ( - element_has_text, - generate_course_key, - is_option_value_selected, - select_option_by_value -) -from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest - - -class StudioSettingsDetailsTest(StudioCourseTest): - """Base class for settings and details page tests.""" - shard = 4 - - def setUp(self, is_staff=True): - super(StudioSettingsDetailsTest, self).setUp(is_staff=is_staff) - self.settings_detail = SettingsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - # Before every test, make sure to visit the page first - self.settings_detail.visit() - - -class SettingsMilestonesTest(StudioSettingsDetailsTest): - """ - Tests for milestones feature in Studio's settings tab - """ - shard = 4 - - def test_page_has_prerequisite_field(self): - """ - Test to make sure page has pre-requisite course field if milestones app is enabled. - """ - - self.assertTrue(self.settings_detail.pre_requisite_course_options) - - def test_prerequisite_course_save_successfully(self): - """ - Scenario: Selecting course from Pre-Requisite course drop down save the selected course as pre-requisite - course. - Given that I am on the Schedule & Details page on studio - When I select an item in pre-requisite course drop down and click Save Changes button - Then My selected item should be saved as pre-requisite course - And My selected item should be selected after refreshing the page.' - """ - course_number = self.unique_id - CourseFixture( - org='test_org', - number=course_number, - run='test_run', - display_name='Test Course' + course_number - ).install() - - pre_requisite_course_key = generate_course_key( - org='test_org', - number=course_number, - run='test_run' - ) - pre_requisite_course_id = six.text_type(pre_requisite_course_key) - - # Refresh the page to load the new course fixture and populate the prrequisite course dropdown - # Then select the prerequisite course and save the changes - self.settings_detail.refresh_page() - self.settings_detail.wait_for_prerequisite_course_options() - select_option_by_value( - browser_query=self.settings_detail.pre_requisite_course_options, - value=pre_requisite_course_id - ) - self.settings_detail.save_changes() - self.assertEqual( - 'Your changes have been saved.', - self.settings_detail.alert_confirmation_title.text - ) - - # Refresh the page again and confirm the prerequisite course selection is properly reflected - self.settings_detail.refresh_page() - self.settings_detail.wait_for_prerequisite_course_options() - self.assertTrue(is_option_value_selected( - browser_query=self.settings_detail.pre_requisite_course_options, - value=pre_requisite_course_id - )) - - # Set the prerequisite course back to None and save the changes - select_option_by_value( - browser_query=self.settings_detail.pre_requisite_course_options, - value='' - ) - self.settings_detail.save_changes() - self.assertEqual( - 'Your changes have been saved.', - self.settings_detail.alert_confirmation_title.text - ) - - # Refresh the page again to confirm the None selection is properly reflected - self.settings_detail.refresh_page() - self.settings_detail.wait_for_prerequisite_course_options() - self.assertTrue(is_option_value_selected( - browser_query=self.settings_detail.pre_requisite_course_options, - value='' - )) - - # Re-pick the prerequisite course and confirm no errors are thrown (covers a discovered bug) - select_option_by_value( - browser_query=self.settings_detail.pre_requisite_course_options, - value=pre_requisite_course_id - ) - self.settings_detail.save_changes() - self.assertEqual( - 'Your changes have been saved.', - self.settings_detail.alert_confirmation_title.text - ) - - # Refresh the page again to confirm the prerequisite course selection is properly reflected - self.settings_detail.refresh_page() - self.settings_detail.wait_for_prerequisite_course_options() - dropdown_status = is_option_value_selected( - browser_query=self.settings_detail.pre_requisite_course_options, - value=pre_requisite_course_id - ) - self.assertTrue(dropdown_status) - - def test_page_has_enable_entrance_exam_field(self): - """ - Test to make sure page has 'enable entrance exam' field. - """ - self.assertTrue(self.settings_detail.entrance_exam_field) - - def test_entrance_exam_has_unit_button(self): - """ - Test that entrance exam should be created after checking the 'enable entrance exam' checkbox. - And user has option to add units only instead of any Subsection. - """ - self.settings_detail.require_entrance_exam(required=True) - self.settings_detail.save_changes() - - # getting the course outline page. - course_outline_page = CourseOutlinePage( - self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'] - ) - course_outline_page.visit() - course_outline_page.wait_for_ajax() - - # button with text 'New Unit' should be present. - self.assertTrue(element_has_text( - page=course_outline_page, - css_selector='.add-item a.button-new', - text='New Unit' - )) - - # button with text 'New Subsection' should not be present. - self.assertFalse(element_has_text( - page=course_outline_page, - css_selector='.add-item a.button-new', - text='New Subsection' - )) - - -class CoursePacingTest(StudioSettingsDetailsTest): - """Tests for setting a course to self-paced.""" - shard = 4 - - def populate_course_fixture(self, __): - ConfigModelFixture('/config/self_paced', {'enabled': True}).install() - # Set the course start date to tomorrow in order to allow setting pacing - self.course_fixture.add_course_details({'start_date': datetime.now() + timedelta(days=1)}) - - def test_default_instructor_paced(self): - """ - Test that the 'instructor paced' button is checked by default. - """ - self.assertEqual(self.settings_detail.course_pacing, 'Instructor-Paced') - - def test_self_paced(self): - """ - Test that the 'self-paced' button is checked for a self-paced - course. - """ - self.course_fixture.add_course_details({ - 'self_paced': True - }) - self.course_fixture.configure_course() - self.settings_detail.refresh_page() - self.assertEqual(self.settings_detail.course_pacing, 'Self-Paced') - - def test_set_self_paced(self): - """ - Test that the self-paced option is persisted correctly. - """ - self.settings_detail.course_pacing = 'Self-Paced' - self.settings_detail.save_changes() - self.settings_detail.refresh_page() - self.assertEqual(self.settings_detail.course_pacing, 'Self-Paced') - - def test_toggle_pacing_after_course_start(self): - """ - Test that course authors cannot toggle the pacing of their course - while the course is running. - """ - self.course_fixture.add_course_details({'start_date': datetime.now()}) - self.course_fixture.configure_course() - self.settings_detail.refresh_page() - self.assertTrue(self.settings_detail.course_pacing_disabled()) - self.assertIn('Course pacing cannot be changed', self.settings_detail.course_pacing_disabled_text) diff --git a/common/test/acceptance/tests/test_cohorted_courseware.py b/common/test/acceptance/tests/test_cohorted_courseware.py deleted file mode 100644 index d8104527cb..0000000000 --- a/common/test/acceptance/tests/test_cohorted_courseware.py +++ /dev/null @@ -1,261 +0,0 @@ -""" -End-to-end test for cohorted courseware. This uses both Studio and LMS. -""" - - -from bok_choy.page_object import XSS_INJECTION - -from common.test.acceptance.fixtures.course import XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.common.utils import add_enrollment_course_modes, enroll_user_track -from common.test.acceptance.pages.lms.courseware import CoursewarePage -from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage -from common.test.acceptance.pages.studio.settings_group_configurations import GroupConfigurationsPage -from common.test.acceptance.pages.studio.xblock_editor import XBlockVisibilityEditorView -from common.test.acceptance.tests.discussion.helpers import CohortTestMixin -from common.test.acceptance.tests.lms.test_lms_user_preview import verify_expected_problem_visibility - -from .studio.base_studio_test import ContainerBase - -AUDIT_TRACK = "Audit" -VERIFIED_TRACK = "Verified" - - -class EndToEndCohortedCoursewareTest(ContainerBase, CohortTestMixin): - """ - End-to-end of cohorted courseware. - """ - shard = 5 - - def setUp(self, is_staff=True): - - super(EndToEndCohortedCoursewareTest, self).setUp(is_staff=is_staff) - self.staff_user = self.user - - self.content_group_a = "Content Group A" + XSS_INJECTION - self.content_group_b = "Content Group B" + XSS_INJECTION - - # Creates the Course modes needed to test enrollment tracks - add_enrollment_course_modes(self.browser, self.course_id, ["audit", "verified"]) - - # Create a student who will be in "Cohort A" - self.cohort_a_student_username = "cohort_a_student" - self.cohort_a_student_email = "cohort_a_student@example.com" - AutoAuthPage( - self.browser, username=self.cohort_a_student_username, email=self.cohort_a_student_email, no_login=True - ).visit() - - # Create a student who will be in "Cohort B" - self.cohort_b_student_username = "cohort_b_student" - self.cohort_b_student_email = "cohort_b_student@example.com" - AutoAuthPage( - self.browser, username=self.cohort_b_student_username, email=self.cohort_b_student_email, no_login=True - ).visit() - - # Create a Verified Student - self.cohort_verified_student_username = "cohort_verified_student" - self.cohort_verified_student_email = "cohort_verified_student@example.com" - AutoAuthPage( - self.browser, - username=self.cohort_verified_student_username, - email=self.cohort_verified_student_email, - no_login=True - ).visit() - - # Create audit student - self.cohort_audit_student_username = "cohort_audit_student" - self.cohort_audit_student_email = "cohort_audit_student@example.com" - AutoAuthPage( - self.browser, - username=self.cohort_audit_student_username, - email=self.cohort_audit_student_email, - no_login=True - ).visit() - - # Create a student who will end up in the default cohort group - self.cohort_default_student_username = "cohort_default_student" - self.cohort_default_student_email = "cohort_default_student@example.com" - AutoAuthPage( - self.browser, username=self.cohort_default_student_username, - email=self.cohort_default_student_email, no_login=True - ).visit() - - # Start logged in as the staff user. - AutoAuthPage( - self.browser, username=self.staff_user["username"], email=self.staff_user["email"] - ).visit() - - def populate_course_fixture(self, course_fixture): - """ - Populate the children of the test course fixture. - """ - self.group_a_problem = 'GROUP A CONTENT' - self.group_b_problem = 'GROUP B CONTENT' - self.group_verified_problem = 'GROUP VERIFIED CONTENT' - self.group_audit_problem = 'GROUP AUDIT CONTENT' - - self.group_a_and_b_problem = 'GROUP A AND B CONTENT' - - self.visible_to_all_problem = 'VISIBLE TO ALL CONTENT' - course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('problem', self.group_a_problem, data=''), - XBlockFixtureDesc('problem', self.group_b_problem, data=''), - XBlockFixtureDesc('problem', self.group_verified_problem, data=''), - XBlockFixtureDesc('problem', self.group_audit_problem, data=''), - XBlockFixtureDesc('problem', self.group_a_and_b_problem, data=''), - XBlockFixtureDesc('problem', self.visible_to_all_problem, data='') - ) - ) - ) - ) - - def create_content_groups(self): - """ - Creates two content groups in Studio Group Configurations Settings. - """ - group_configurations_page = GroupConfigurationsPage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - group_configurations_page.visit() - - group_configurations_page.create_first_content_group() - config = group_configurations_page.content_groups[0] - config.name = self.content_group_a - config.save() - - group_configurations_page.add_content_group() - config = group_configurations_page.content_groups[1] - config.name = self.content_group_b - config.save() - - def link_problems_to_content_groups_and_publish(self): - """ - Updates 5 of the 6 existing problems to limit their visibility by content group. - Publishes the modified units. - """ - container_page = self.go_to_unit_page() - enrollment_group = 'enrollment_track_group' - - def set_visibility(problem_index, groups, group_partition='content_group'): - problem = container_page.xblocks[problem_index] - problem.edit_visibility() - visibility_dialog = XBlockVisibilityEditorView(self.browser, problem.locator) - partition_name = (visibility_dialog.ENROLLMENT_TRACK_PARTITION - if group_partition == enrollment_group - else visibility_dialog.CONTENT_GROUP_PARTITION) - visibility_dialog.select_groups_in_partition_scheme(partition_name, groups) - - set_visibility(1, [self.content_group_a]) - set_visibility(2, [self.content_group_b]) - set_visibility(3, [VERIFIED_TRACK], enrollment_group) - set_visibility(4, [AUDIT_TRACK], enrollment_group) - set_visibility(5, [self.content_group_a, self.content_group_b]) - - container_page.publish() - - def create_cohorts_and_assign_students(self): - """ - Adds 2 manual cohorts, linked to content groups, to the course. - Each cohort is assigned one student. - """ - instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id) - instructor_dashboard_page.visit() - cohort_management_page = instructor_dashboard_page.select_cohort_management() - - def add_cohort_with_student(cohort_name, content_group, student): - cohort_management_page.add_cohort(cohort_name, content_group=content_group) - cohort_management_page.add_students_to_selected_cohort([student]) - - add_cohort_with_student("Cohort A", self.content_group_a, self.cohort_a_student_username) - add_cohort_with_student("Cohort B", self.content_group_b, self.cohort_b_student_username) - - def view_cohorted_content_as_different_users(self): - """ - View content as staff, student in Cohort A, student in Cohort B, Verified Student, Audit student, - and student in Default Cohort. - """ - courseware_page = CoursewarePage(self.browser, self.course_id) - - def login_and_verify_visible_problems(username, email, expected_problems, track=None): - AutoAuthPage( - self.browser, username=username, email=email, course_id=self.course_id - ).visit() - if track is not None: - enroll_user_track(self.browser, self.course_id, track) - courseware_page.visit() - verify_expected_problem_visibility(self, courseware_page, expected_problems) - - login_and_verify_visible_problems( - self.staff_user["username"], self.staff_user["email"], - [self.group_a_problem, - self.group_b_problem, - self.group_verified_problem, - self.group_audit_problem, - self.group_a_and_b_problem, - self.visible_to_all_problem - ], - ) - - login_and_verify_visible_problems( - self.cohort_a_student_username, self.cohort_a_student_email, - [self.group_a_problem, self.group_audit_problem, self.group_a_and_b_problem, self.visible_to_all_problem] - ) - - login_and_verify_visible_problems( - self.cohort_b_student_username, self.cohort_b_student_email, - [self.group_b_problem, self.group_audit_problem, self.group_a_and_b_problem, self.visible_to_all_problem] - ) - - login_and_verify_visible_problems( - self.cohort_verified_student_username, self.cohort_verified_student_email, - [self.group_verified_problem, self.visible_to_all_problem], - 'verified' - ) - - login_and_verify_visible_problems( - self.cohort_audit_student_username, self.cohort_audit_student_email, - [self.group_audit_problem, self.visible_to_all_problem], - 'audit' - ) - - login_and_verify_visible_problems( - self.cohort_default_student_username, self.cohort_default_student_email, - [self.group_audit_problem, self.visible_to_all_problem], - ) - - def test_cohorted_courseware(self): - """ - Scenario: Can create content that is only visible to students in particular cohorts - Given that I have course with 6 problems, 1 staff member, and 6 students - When I enable cohorts in the course - And I add the Course Modes for Verified and Audit - And I create two content groups, Content Group A, and Content Group B, in the course - And I link one problem to Content Group A - And I link one problem to Content Group B - And I link one problem to the Verified Group - And I link one problem to the Audit Group - And I link one problem to both Content Group A and Content Group B - And one problem remains unlinked to any Content Group - And I create two manual cohorts, Cohort A and Cohort B, - linked to Content Group A and Content Group B, respectively - And I assign one student to each manual cohort - And I assign one student to each enrollment track - And one student remains in the default cohort - Then the staff member can see all 6 problems - And the student in Cohort A can see all the problems linked to A - And the student in Cohort B can see all the problems linked to B - And the student in Verified can see the problems linked to Verified and those not linked to a Group - And the student in Audit can see the problems linked to Audit and those not linked to a Group - And the student in the default cohort can ony see the problem that is unlinked to any Content Group - """ - self.enable_cohorting(self.course_fixture) - self.create_content_groups() - self.link_problems_to_content_groups_and_publish() - self.create_cohorts_and_assign_students() - self.view_cohorted_content_as_different_users() diff --git a/common/test/acceptance/tests/video/test_studio_video_editor.py b/common/test/acceptance/tests/video/test_studio_video_editor.py deleted file mode 100644 index 951021a871..0000000000 --- a/common/test/acceptance/tests/video/test_studio_video_editor.py +++ /dev/null @@ -1,466 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Acceptance tests for CMS Video Editor. -""" - - -import ddt - -from common.test.acceptance.pages.common.utils import confirm_prompt -from common.test.acceptance.tests.video.test_studio_video_module import CMSVideoBaseTest - - -@ddt.ddt -class VideoEditorTest(CMSVideoBaseTest): - """ - CMS Video Editor Test Class - """ - shard = 6 - - def _create_video_component(self, subtitles=False): - """ - Create a video component and navigate to unit page - - Arguments: - subtitles (bool): Upload subtitles or not - - """ - if subtitles: - self.assets.append('subs_3_yD_cEKoCk.srt.sjson') - - self.navigate_to_course_unit() - - def test_default_settings(self): - """ - Scenario: User can view Video metadata - Given I have created a Video component - And I edit the component - Then I see the correct video settings and default values - """ - self._create_video_component() - self.edit_component() - self.assertTrue(self.video.verify_settings()) - - def test_modify_video_display_name(self): - """ - Scenario: User can modify Video display name - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - Then I can modify video display name - And my video display name change is persisted on save - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.set_field_value('Component Display Name', 'Transformers') - self.save_unit_settings() - self.edit_component() - self.open_advanced_tab() - self.assertTrue(self.video.verify_field_value('Component Display Name', 'Transformers')) - - def test_hidden_captions(self): - """ - Scenario: Captions are hidden when "transcript display" is false - Given I have created a Video component with subtitles - And I have set "transcript display" to False - Then when I view the video it does not show the captions - """ - self._create_video_component(subtitles=True) - # Prevent cookies from overriding course settings - self.browser.delete_cookie('hide_captions') - self.edit_component() - self.open_advanced_tab() - self.video.set_field_value('Show Transcript', 'False', 'select') - self.save_unit_settings() - self.assertFalse(self.video.is_captions_visible()) - - def test_translations_uploading(self): - """ - Scenario: Translations uploading works correctly - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "chinese_transcripts.srt" for "zh" language code - And I save changes - Then when I view the video it does show the captions - And I see "好 各位同学" text in the captions - And I edit the component - And I open tab "Advanced" - And I see translations for "zh" - And I upload transcript file "uk_transcripts.srt" for "uk" language code - And I save changes - Then when I view the video it does show the captions - And I see "好 各位同学" text in the captions - And video language menu has "uk, zh" translations - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('chinese_transcripts.srt', 'zh') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - unicode_text = u"好 各位同学" - self.assertIn(unicode_text, self.video.captions_text) - self.edit_component() - self.open_advanced_tab() - self.assertEqual(self.video.translations(), ['zh']) - self.video.upload_translation('uk_transcripts.srt', 'uk') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - self.assertIn(unicode_text, self.video.captions_text) - self.assertEqual(set(self.video.caption_languages.keys()), {'zh', 'uk'}) - - def test_save_language_upload_no_transcript(self): - """ - Scenario: Transcript language is not shown in language menu if no transcript file is uploaded - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I add a language "uk" but do not upload an .srt file - And I save changes - When I view the video language menu - Then I am not able to see the language "uk" translation language - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - language_code = 'uk' - self.video.click_button('translation_add') - translations_count = self.video.translations_count() - self.video.select_translation_language(language_code, translations_count - 1) - self.save_unit_settings() - self.assertNotIn(language_code, list(self.video.caption_languages.keys())) - - def test_upload_large_transcript(self): - """ - Scenario: User can upload transcript file with > 1mb size - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "1mb_transcripts.srt" for "uk" language code - And I save changes - Then when I view the video it does show the captions - And I see "Привіт, edX вітає вас." text in the captions - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('1mb_transcripts.srt', 'uk') - self.save_unit_settings() - self.video.wait_for(self.video.is_captions_visible, 'Captions are visible', timeout=10) - unicode_text = u"Привіт, edX вітає вас." - self.assertIn(unicode_text, self.video.captions_lines()) - - def test_translations_download_works_w_saving(self): - """ - Scenario: Translations downloading works correctly w/ preliminary saving - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript files: - |lang_code|filename | - |uk |uk_transcripts.srt | - |zh |chinese_transcripts.srt| - And I save changes - And I edit the component - And I open tab "Advanced" - And I see translations for "uk, zh" - And video language menu has "uk, zh" translations - Then I can download transcript for "zh" language code, that contains text "好 各位同学" - And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас." - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('uk_transcripts.srt', 'uk') - self.video.upload_translation('chinese_transcripts.srt', 'zh') - self.save_unit_settings() - self.edit_component() - self.open_advanced_tab() - self.assertEqual(sorted(self.video.translations()), sorted(['zh', 'uk'])) - self.assertEqual(sorted(list(self.video.caption_languages.keys())), sorted(['zh', 'uk'])) - zh_unicode_text = u"好 各位同学" - self.assertTrue(self.video.download_translation('zh', zh_unicode_text)) - uk_unicode_text = u"Привіт, edX вітає вас." - self.assertTrue(self.video.download_translation('uk', uk_unicode_text)) - - def test_translations_download_works_wo_saving(self): - """ - Scenario: Translations downloading works correctly w/o preliminary saving - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript files: - |lang_code|filename | - |uk |uk_transcripts.srt | - |zh |chinese_transcripts.srt| - Then I can download transcript for "zh" language code, that contains text "好 各位同学" - And I can download transcript for "uk" language code, that contains text "Привіт, edX вітає вас." - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('uk_transcripts.srt', 'uk') - self.video.upload_translation('chinese_transcripts.srt', 'zh') - zh_unicode_text = u"好 各位同学" - self.assertTrue(self.video.download_translation('zh', zh_unicode_text)) - uk_unicode_text = u"Привіт, edX вітає вас." - self.assertTrue(self.video.download_translation('uk', uk_unicode_text)) - - def test_translations_remove_works_wo_saving(self): - """ - Scenario: Translations removing works correctly w/o preliminary saving - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "uk_transcripts.srt" for "uk" language code - And I see translations for "uk" - Then I remove translation for "uk" language code - And I save changes - Then when I view the video it does not show the captions - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('uk_transcripts.srt', 'uk') - self.assertEqual(self.video.translations(), ['uk']) - self.video.remove_translation('uk') - confirm_prompt(self.video) - self.save_unit_settings() - self.assertFalse(self.video.is_captions_visible()) - - def test_translations_entry_remove_works(self): - """ - Scenario: Translations entry removal works correctly when transcript is not uploaded - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I click on "+ Add" button for "Transcript Languages" field - Then I click on "Remove" button - And I see newly created entry is removed - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.click_button("translation_add") - self.assertEqual(self.video.translations_count(), 1) - self.video.remove_translation("") - self.assertEqual(self.video.translations_count(), 0) - - def test_cannot_upload_sjson_translation(self): - """ - Scenario: User cannot upload translations in sjson format - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I click button "Add" - And I choose "uk" language code - And I try to upload transcript file "subs_3_yD_cEKoCk.srt.sjson" - Then I see validation error "Only SRT files can be uploaded. Please select a file ending in .srt to upload." - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.click_button('translation_add') - self.video.select_translation_language('uk') - self.video.upload_asset('subs_3_yD_cEKoCk.srt.sjson', asset_type='transcript') - error_msg = 'Only SRT files can be uploaded. Please select a file ending in .srt to upload.' - self.assertEqual(self.video.upload_status_message, error_msg) - - def test_replace_translation_w_save(self): - """ - Scenario: User can easy replace the translation by another one w/ preliminary saving - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "chinese_transcripts.srt" for "zh" language code - And I save changes - Then when I view the video it does show the captions - And I see "好 各位同学" text in the captions - And I edit the component - And I open tab "Advanced" - And I see translations for "zh" - And I replace transcript file for "zh" language code by "uk_transcripts.srt" - And I save changes - Then when I view the video it does show the captions - And I see "Привіт, edX вітає вас." text in the captions - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('chinese_transcripts.srt', 'zh') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - unicode_text = u"好 各位同学" - self.assertIn(unicode_text, self.video.captions_text) - self.edit_component() - self.open_advanced_tab() - self.assertEqual(self.video.translations(), ['zh']) - self.video.replace_translation('zh', 'uk', 'uk_transcripts.srt') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - unicode_text = u"Привіт, edX вітає вас." - self.assertIn(unicode_text, self.video.captions_text) - - def test_replace_translation_wo_save(self): - """ - Scenario: User can easy replace the translation by another one w/o preliminary saving - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "chinese_transcripts.srt" for "zh" language code - And I see translations for "zh" - And I replace transcript file for "zh" language code by "uk_transcripts.srt" - And I save changes - Then when I view the video it does show the captions - And I see "Привіт, edX вітає вас." text in the captions - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('chinese_transcripts.srt', 'zh') - self.assertEqual(self.video.translations(), ['zh']) - self.video.replace_translation('zh', 'uk', 'uk_transcripts.srt') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - unicode_text = u"Привіт, edX вітає вас." - self.assertIn(unicode_text, self.video.captions_text) - - def test_translation_upload_remove_upload(self): - """ - Scenario: Upload "zh" file "A" -> Remove "zh" -> Upload "zh" file "B" - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "chinese_transcripts.srt" for "zh" language code - And I see translations for "zh" - Then I remove translation for "zh" language code - And I upload transcript file "uk_transcripts.srt" for "zh" language code - And I save changes - Then when I view the video it does show the captions - And I see "Привіт, edX вітає вас." text in the captions - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('chinese_transcripts.srt', 'zh') - self.assertEqual(self.video.translations(), ['zh']) - self.video.remove_translation('zh') - confirm_prompt(self.video) - self.video.upload_translation('uk_transcripts.srt', 'zh') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - unicode_text = u"Привіт, edX вітає вас." - self.assertIn(unicode_text, self.video.captions_text) - - def test_select_language_twice(self): - """ - Scenario: User cannot select the same language twice - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I click button "Add" - And I choose "zh" language code - And I click button "Add" - Then I cannot choose "zh" language code - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.click_button('translation_add') - self.video.select_translation_language('zh') - self.video.click_button('translation_add') - self.assertTrue(self.video.is_language_disabled('zh')) - - def test_table_of_contents(self): - """ - Scenario: User can see Abkhazian (ab) language option at the first position - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript files: - |lang_code|filename | - |uk |uk_transcripts.srt | - |table |chinese_transcripts.srt| - And I save changes - Then when I view the video it does show the captions - And I see "好 各位同学" text in the captions - And video language menu has "table, uk" translations - And I see video language with code "table" at position "0" - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('uk_transcripts.srt', 'uk') - self.video.upload_translation('chinese_transcripts.srt', 'ab') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - unicode_text = u"好 各位同学" - self.assertIn(unicode_text, self.video.captions_text) - self.assertEqual(sorted(list(self.video.caption_languages.keys())), sorted([u'ab', u'uk'])) - self.assertEqual(sorted(list(self.video.caption_languages.keys()))[0], 'ab') - - def test_upload_transcript_with_BOM(self): - """ - Scenario: User can upload transcript file with BOM(Byte Order Mark) in it. - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "chinese_transcripts_with_BOM.srt" for "zh" language code - And I save changes - Then when I view the video it does show the captions - And I see "莎拉·佩林 (Sarah Palin)" text in the captions - """ - self._create_video_component() - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation('chinese_transcripts_with_BOM.srt', 'zh') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - unicode_text = u"莎拉·佩林 (Sarah Palin)" - self.assertIn(unicode_text, self.video.captions_lines()) - - def test_simplified_and_traditional_chinese_transcripts_uploading(self): - """ - Scenario: Translations uploading works correctly - - Given I have created a Video component - And I edit the component - And I open tab "Advanced" - And I upload transcript file "simplified_chinese.srt" for "zh_HANS" language code - And I save changes - Then when I view the video it does show the captions - And I see "在线学习是革" text in the captions - - And I edit the component - And I open tab "Advanced" - And I upload transcript file "traditional_chinese.srt" for "zh_HANT" language code - And I save changes - Then when I view the video it does show the captions - And I see "在線學習是革" text in the captions - - And video subtitle menu has 'zh_HANS', 'zh_HANT' translations for 'Simplified Chinese' - and 'Traditional Chinese' respectively - """ - self._create_video_component() - - langs_info = [ - ('zh_HANS', 'simplified_chinese.srt', u'在线学习是革'), - ('zh_HANT', 'traditional_chinese.srt', u'在線學習是革') - ] - - for lang_code, lang_file, lang_text in langs_info: - self.edit_component() - self.open_advanced_tab() - self.video.upload_translation(lang_file, lang_code) - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - # If there is only one language then there will be no subtitle/captions menu - if lang_code == u'zh_HANT': - self.video.select_language(lang_code) - unicode_text = lang_text - self.assertIn(unicode_text, self.video.captions_text) - - self.assertEqual(self.video.caption_languages, {'zh_HANS': 'Simplified Chinese', 'zh_HANT': 'Traditional Chinese'}) diff --git a/common/test/acceptance/tests/video/test_studio_video_module.py b/common/test/acceptance/tests/video/test_studio_video_module.py deleted file mode 100644 index 0cbd37e374..0000000000 --- a/common/test/acceptance/tests/video/test_studio_video_module.py +++ /dev/null @@ -1,371 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Acceptance tests for CMS Video Module. -""" - - -import os -from unittest import skipIf - -from bok_choy.promise import EmptyPromise -from mock import patch - -from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc -from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.studio.overview import CourseOutlinePage -from common.test.acceptance.pages.studio.video.video import VideoComponentPage -from common.test.acceptance.tests.helpers import UniqueCourseTest, YouTubeStubConfig, is_youtube_available - - -@skipIf(is_youtube_available() is False, 'YouTube is not available!') -class CMSVideoBaseTest(UniqueCourseTest): - """ - CMS Video Module Base Test Class - """ - - def setUp(self): - """ - Initialization of pages and course fixture for tests - """ - super(CMSVideoBaseTest, self).setUp() - - self.video = VideoComponentPage(self.browser) - - # This will be initialized later - self.unit_page = None - - self.outline = CourseOutlinePage( - self.browser, - self.course_info['org'], - self.course_info['number'], - self.course_info['run'] - ) - - self.course_fixture = CourseFixture( - self.course_info['org'], self.course_info['number'], - self.course_info['run'], self.course_info['display_name'] - ) - - self.assets = [] - self.metadata = None - self.addCleanup(YouTubeStubConfig.reset) - - def _create_course_unit(self, youtube_stub_config=None, subtitles=False): - """ - Create a Studio Video Course Unit and Navigate to it. - - Arguments: - youtube_stub_config (dict) - subtitles (bool) - - """ - if youtube_stub_config: - YouTubeStubConfig.configure(youtube_stub_config) - - if subtitles: - self.assets.append('subs_3_yD_cEKoCk.srt.sjson') - - self.navigate_to_course_unit() - - def _create_video(self): - """ - Create Xblock Video Component. - """ - self.video.create_video() - - video_xblocks = self.video.xblocks() - - # Total video xblock components count should be equals to 2 - # Why 2? One video component is created by default for each test. Please see - # test_studio_video_module.py:CMSVideoTest._create_course_unit - # And we are creating second video component here. - self.assertEqual(video_xblocks, 2) - - def _install_course_fixture(self): - """ - Prepare for tests by creating a course with a section, subsection, and unit. - Performs the following: - Create a course with a section, subsection, and unit - Create a user and make that user a course author - Log the user into studio - """ - - if self.assets: - self.course_fixture.add_asset(self.assets) - - # Create course with Video component - self.course_fixture.add_children( - XBlockFixtureDesc('chapter', 'Test Section').add_children( - XBlockFixtureDesc('sequential', 'Test Subsection').add_children( - XBlockFixtureDesc('vertical', 'Test Unit').add_children( - XBlockFixtureDesc('video', 'Video', metadata=self.metadata) - ) - ) - ) - ).install() - - # Auto login and register the course - AutoAuthPage( - self.browser, - staff=False, - username=self.course_fixture.user.get('username'), - email=self.course_fixture.user.get('email'), - password=self.course_fixture.user.get('password') - ).visit() - - def _navigate_to_course_unit_page(self): - """ - Open the course from the dashboard and expand the section and subsection and click on the Unit link - The end result is the page where the user is editing the newly created unit - """ - # Visit Course Outline page - self.outline.visit() - - # Visit Unit page - self.unit_page = self.outline.section('Test Section').subsection('Test Subsection').expand_subsection().unit( - 'Test Unit').go_to() - - self.video.wait_for_video_component_render() - - def navigate_to_course_unit(self): - """ - Install the course with required components and navigate to course unit page - """ - self._install_course_fixture() - self._navigate_to_course_unit_page() - - def edit_component(self, xblock_index=1): - """ - Open component Edit Dialog for first component on page. - - Arguments: - xblock_index: number starting from 1 (0th entry is the unit page itself) - """ - self.unit_page.xblocks[xblock_index].edit() - EmptyPromise( - lambda: self.video.q(css='div.basic_metadata_edit').visible, - "Wait for the basic editor to be open", - timeout=5 - ).fulfill() - - def open_advanced_tab(self): - """ - Open components advanced tab. - """ - # The 0th entry is the unit page itself. - self.unit_page.xblocks[1].open_advanced_tab() - - def open_basic_tab(self): - """ - Open components basic tab. - """ - # The 0th entry is the unit page itself. - self.unit_page.xblocks[1].open_basic_tab() - - def save_unit_settings(self): - """ - Save component settings. - """ - # The 0th entry is the unit page itself. - self.unit_page.xblocks[1].save_settings() - - -class CMSVideoTest(CMSVideoBaseTest): - """ - CMS Video Test Class - """ - shard = 13 - - def test_youtube_stub_proxy(self): - """ - Scenario: YouTube stub server proxies YouTube API correctly - Given youtube stub server proxies YouTube API - And I have created a Video component - Then I can see video button "play" - And I click video button "play" - Then I can see video button "pause" - """ - self._create_course_unit(youtube_stub_config={'youtube_api_blocked': False}) - - self.assertTrue(self.video.is_button_shown('play')) - self.video.click_player_button('play') - self.video.wait_for_state('playing') - self.assertTrue(self.video.is_button_shown('pause')) - - def test_youtube_stub_blocks_youtube_api(self): - """ - Scenario: YouTube stub server can block YouTube API - Given youtube stub server blocks YouTube API - And I have created a Video component - Then I do not see video button "play" - """ - self._create_course_unit(youtube_stub_config={'youtube_api_blocked': True}) - - self.assertFalse(self.video.is_button_shown('play')) - - def test_autoplay_is_disabled(self): - """ - Scenario: Autoplay is disabled in Studio - Given I have created a Video component - Then when I view the video it does not have autoplay enabled - """ - self._create_course_unit() - - self.assertFalse(self.video.is_autoplay_enabled) - - def test_video_creation_takes_single_click(self): - """ - Scenario: Creating a video takes a single click - And creating a video takes a single click - """ - self._create_course_unit() - - # This will create a video by doing a single click and then ensure that video is created - self._create_video() - - def test_captions_hidden_correctly(self): - """ - Scenario: Captions are hidden correctly - Given I have created a Video component with subtitles - And I have hidden captions - Then when I view the video it does not show the captions - """ - self._create_course_unit(subtitles=True) - - self.video.hide_captions() - - self.assertFalse(self.video.is_captions_visible()) - - def test_video_controls_shown_correctly(self): - """ - Scenario: Video controls for all videos show correctly - Given I have created two Video components - And first is private video - When I reload the page - Then video controls for all videos are visible - And the error message isn't shown - """ - self._create_course_unit(youtube_stub_config={'youtube_api_private_video': True}) - self.video.create_video() - - # change id of first default video - self.edit_component(1) - self.open_advanced_tab() - self.video.set_field_value('YouTube ID', 'sampleid123') - self.save_unit_settings() - - # again open unit page and check that video controls show for both videos - self._navigate_to_course_unit_page() - self.assertTrue(self.video.is_controls_visible()) - - # verify that the error message isn't shown by default - self.assertFalse(self.video.is_error_message_shown) - - def test_captions_shown_correctly(self): - """ - Scenario: Captions are shown correctly - Given I have created a Video component with subtitles - Then when I view the video it does show the captions - """ - self._create_course_unit(subtitles=True) - self.assertTrue(self.video.is_captions_visible()) - - def test_captions_toggling(self): - """ - Scenario: Captions are toggled correctly - Given I have created a Video component with subtitles - And I have toggled captions - Then when I view the video it does show the captions - """ - self._create_course_unit(subtitles=True) - - self.video.click_player_button('transcript_button') - - self.assertFalse(self.video.is_captions_visible()) - - self.video.click_player_button('transcript_button') - - self.assertTrue(self.video.is_captions_visible()) - - def test_transcript_state_is_saved_on_reload(self): - """ - Scenario: Transcripts state is preserved - Given I have created a Video component with subtitles - And I have toggled off the transcript - After page reload transcript is already off - Then when I view the video it does show the captions - """ - self._create_course_unit(subtitles=True) - self.video.click_player_button('transcript_button') - self.assertFalse(self.video.is_captions_visible()) - self.video.click_player_button('transcript_button') - self.assertTrue(self.video.is_captions_visible()) - self.browser.refresh() - self.assertTrue(self.video.is_captions_visible()) - - def test_caption_line_focus(self): - """ - Scenario: When enter key is pressed on a caption, an outline shows around it - Given I have created a Video component with subtitles - And Make sure captions are opened - Then I focus on first caption line - And I see first caption line has focused - """ - self._create_course_unit(subtitles=True) - - self.video.show_captions() - - self.video.focus_caption_line(2) - - self.assertTrue(self.video.is_caption_line_focused(2)) - - def test_slider_range_works(self): - """ - Scenario: When start and end times are specified, a range on slider is shown - Given I have created a Video component with subtitles - And Make sure captions are closed - And I edit the component - And I open tab "Advanced" - And I set value "00:00:12" to the field "Video Start Time" - And I set value "00:00:24" to the field "Video Stop Time" - And I save changes - And I click video button "play" - Then I see a range on slider - """ - self._create_course_unit(subtitles=True) - - self.video.hide_captions() - - self.edit_component() - - self.open_advanced_tab() - - self.video.set_field_value('Video Start Time', '00:00:12') - - self.video.set_field_value('Video Stop Time', '00:00:24') - - self.save_unit_settings() - - self.video.click_player_button('play') - - -class CMSVideoA11yTest(CMSVideoBaseTest): - """ - CMS Video Accessibility Test Class - """ - a11y = True - - def setUp(self): - browser = os.environ.get('SELENIUM_BROWSER', 'firefox') - - # the a11y tests run in CI under phantomjs which doesn't - # support html5 video or flash player, so the video tests - # don't work in it. We still want to be able to run these - # tests in CI, so override the browser setting if it is - # phantomjs. - if browser == 'phantomjs': - browser = 'firefox' - - with patch.dict(os.environ, {'SELENIUM_BROWSER': browser}): - super(CMSVideoA11yTest, self).setUp() diff --git a/common/test/acceptance/tests/video/test_studio_video_transcript.py b/common/test/acceptance/tests/video/test_studio_video_transcript.py deleted file mode 100644 index 8beb18aa6c..0000000000 --- a/common/test/acceptance/tests/video/test_studio_video_transcript.py +++ /dev/null @@ -1,445 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Acceptance tests for CMS Video Transcripts. - -For transcripts acceptance tests there are 3 available caption -files. They can be used to test various transcripts features. Two of -them can be imported from YouTube. - -The length of each file name is 11 characters. This is because the -YouTube's ID length is 11 characters. If file name is not of length 11, -front-end validation will not pass. - - t__eq_exist - this file exists on YouTube, and can be imported - via the transcripts menu; after import, this file will - be equal to the one stored locally - t_neq_exist - same as above, except local file will differ from the - one stored on YouTube - t_not_exist - this file does not exist on YouTube; it exists locally -""" - - -from common.test.acceptance.tests.video.test_studio_video_module import CMSVideoBaseTest - - -class VideoTranscriptTest(CMSVideoBaseTest): - """ - CMS Video Transcript Test Class - """ - shard = 18 - - def _create_video_component(self, subtitles=False, subtitle_id='3_yD_cEKoCk'): - """ - Create a video component and navigate to unit page - - Arguments: - subtitles (bool): Upload subtitles or not - subtitle_id (str): subtitle file id - - """ - if subtitles: - self.assets.append('subs_{}.srt.sjson'.format(subtitle_id)) - - self.navigate_to_course_unit() - - def test_youtube_id_w_different_local_server_sub(self): - """ - Scenario: Youtube id only: check "Found" state when user sets youtube_id with different local and server subs - Given I have created a Video component with subtitles "t_neq_exist" - - And I enter a "http://youtu.be/t_neq_exist" source to field number 1 - And I see status message "Timed Transcript Conflict" - And I see button "replace" - And I click transcript button "replace" - And I see status message "Timed Transcript Found" - Then I save video component And captions are visible. - """ - self._create_video_component(subtitles=True, subtitle_id='t_neq_exist') - self.edit_component() - - self.video.set_url_field('http://youtu.be/t_neq_exist', 1) - self.assertEqual(self.video.message('status'), 'Timed Transcript Conflict') - self.assertTrue(self.video.is_transcript_button_visible('replace')) - self.video.click_button_subtitles() - self.video.wait_for_message('status', 'Timed Transcript Found') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - - def test_html5_source_w_not_found_state(self): - """ - Scenario: html5 source only: check "Not Found" state - Given I have created a Video component - - And I enter a "t_not_exist.mp4" source to field number 1 - Then I see status message "No Timed Transcript" - """ - self._create_video_component() - self.edit_component() - - self.video.set_url_field('t_not_exist.mp4', 1) - self.assertEqual(self.video.message('status'), 'No Timed Transcript') - - def test_set_youtube_id_wo_local(self): - """ -        Scenario: User sets youtube_id w/o local but with server subs and one html5 link w/o -                  transcripts w/o import action, then another one html5 link w/o transcripts -        Given I have created a Video component - -        urls = ['http://youtu.be/t__eq_exist', 't_not_exist.mp4', 't_not_exist.webm'] -        for each url in urls do the following -            Enter `url` to field number `n` -            Status message `No EdX Timed Transcript` is shown -            `import` and `upload_new_timed_transcripts` are shown -        """ - self._create_video_component() - self.edit_component() - - urls = ['http://youtu.be/t__eq_exist', 't_not_exist.mp4', 't_not_exist.webm'] - for index, url in enumerate(urls, 1): - self.video.set_url_field(url, index) - self.assertEqual(self.video.message('status'), 'No EdX Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('import')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - def test_youtube_with_import(self): - """ - Scenario: Entering youtube with imported transcripts, and 2 html5 sources without transcripts - "Found" - Given I have created a Video component - - And I enter a "http://youtu.be/t__eq_exist" source to field number 1 - Then I see status message "No EdX Timed Transcript" - And I see button "import" - And I click transcript button "import" - Then I see status message "Timed Transcript Found" - And I see button "upload_new_timed_transcripts" - - urls = ['t_not_exist.mp4', 't_not_exist.webm'] - for each url in urls do the following -            Enter `url` to field number `n` -            Status message `Timed Transcript Found` is shown -            `download_to_edit` and `upload_new_timed_transcripts` buttons are shown - """ - self._create_video_component() - self.edit_component() - - self.video.set_url_field('http://youtu.be/t__eq_exist', 1) - self.assertEqual(self.video.message('status'), 'No EdX Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('import')) - self.video.click_button('import') - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - urls = ['t_not_exist.mp4', 't_not_exist.webm'] - for index, url in enumerate(urls, 2): - self.video.set_url_field(url, index) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('download_to_edit')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - def test_youtube_wo_imported_transcripts(self): - """ - Scenario: Entering youtube w/o imported transcripts - html5 w/o transcripts w/o import - html5 with transcripts - Given I have created a Video component with subtitles "t_neq_exist" - - urls = ['http://youtu.be/t__eq_exist', 't_not_exist.mp4', 't_neq_exist.webm'] - for each url in urls do the following -            Enter `url` to field number `n` -            Status message `No EdX Timed Transcript` is shown -            `import` and `upload_new_timed_transcripts` buttons are shown - """ - self._create_video_component(subtitles=True, subtitle_id='t_neq_exist') - self.edit_component() - - urls = ['http://youtu.be/t__eq_exist', 't_not_exist.mp4', 't_neq_exist.webm'] - for index, url in enumerate(urls, 1): - self.video.set_url_field(url, index) - self.assertEqual(self.video.message('status'), 'No EdX Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('import')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - def test_youtube_w_imported_transcripts(self): - """ - Scenario: Entering youtube with imported transcripts - html5 with transcripts - html5 w/o transcripts - Given I have created a Video component with subtitles "t_neq_exist" - - And I enter a "http://youtu.be/t__eq_exist" source to field number 1 - Then I see status message "No EdX Timed Transcript" - And I see button "import" - And I click transcript button "import" - Then I see status message "Timed Transcript Found" - And I see button "upload_new_timed_transcripts" - - urls = ['t_neq_exist.mp4', 't_not_exist.webm'] - for each url in urls do the following -            Enter `url` to field number `n` -            Status message `Timed Transcript Found` is shown -            `download_to_edit` and `upload_new_timed_transcripts` buttons are shown - """ - self._create_video_component(subtitles=True, subtitle_id='t_neq_exist') - self.edit_component() - - self.video.set_url_field('http://youtu.be/t__eq_exist', 1) - self.assertEqual(self.video.message('status'), 'No EdX Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('import')) - self.video.click_button('import') - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - urls = ['t_neq_exist.mp4', 't_not_exist.webm'] - for index, url in enumerate(urls, 2): - self.video.set_url_field(url, index) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('download_to_edit')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - def test_youtube_w_imported_transcripts2(self): - """ - Scenario: Entering youtube with imported transcripts - html5 w/o transcripts - html5 with transcripts - Given I have created a Video component with subtitles "t_neq_exist" - - And I enter a "http://youtu.be/t__eq_exist" source to field number 1 - Then I see status message "No EdX Timed Transcript" - And I see button "import" - And I click transcript button "import" - Then I see status message "Timed Transcript Found" - And I see button "upload_new_timed_transcripts" - - urls = ['t_not_exist.mp4', 't_neq_exist.webm'] - for each url in urls do the following -            Enter `url` to field number `n` -            Status message `Timed Transcript Found` is shown -            `download_to_edit` and `upload_new_timed_transcripts` buttons are shown - """ - self._create_video_component(subtitles=True, subtitle_id='t_neq_exist') - self.edit_component() - - self.video.set_url_field('http://youtu.be/t__eq_exist', 1) - self.assertEqual(self.video.message('status'), 'No EdX Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('import')) - self.video.click_button('import') - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - urls = ['t_not_exist.mp4', 't_neq_exist.webm'] - for index, url in enumerate(urls, 2): - self.video.set_url_field(url, index) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('download_to_edit')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - def test_html5_with_transcripts(self): - """ - Scenario: Entering html5 with transcripts - upload - youtube w/o transcripts - Given I have created a Video component with subtitles "t__eq_exist" - - And I enter a "t__eq_exist.mp4" source to field number 1 - Then I see status message "Timed Transcript Found" - `download_to_edit` and `upload_new_timed_transcripts` buttons are shown - And I upload the transcripts file "uk_transcripts.srt" - Then I see status message "Timed Transcript Uploaded Successfully" - `download_to_edit` and `upload_new_timed_transcripts` buttons are shown - - And I enter a "http://youtu.be/t_not_exist" source to field number 2 - Then I see status message "Timed Transcript Found" - `download_to_edit` and `upload_new_timed_transcripts` buttons are shown - - And I enter a "uk_transcripts.webm" source to field number 3 - Then I see status message "Timed Transcript Found" - """ - self._create_video_component(subtitles=True, subtitle_id='t__eq_exist') - self.edit_component() - - self.video.set_url_field('t__eq_exist.mp4', 1) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('download_to_edit')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - self.video.upload_transcript('uk_transcripts.srt') - self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully') - self.assertTrue(self.video.is_transcript_button_visible('download_to_edit')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - self.video.set_url_field('http://youtu.be/t_not_exist', 2) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('download_to_edit')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - self.video.set_url_field('uk_transcripts.webm', 3) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - - def test_two_fields_only(self): - """ - Scenario: Work with 2 fields: Enter HTML5 source with transcripts - save -> change it to another one HTML5 - source w/o transcripts - do not click on use existing -> add another one HTML5 source w/o - transcripts - click on use existing - Given I have created a Video component with subtitles "t_not_exist" - - And I enter a "t_not_exist.mp4" source to field number 1 - Then I see status message "Timed Transcript Found" - `download_to_edit` and `upload_new_timed_transcripts` buttons are shown - And I save changes - And I edit the component - - And I enter a "video_name_2.mp4" source to field number 1 - Then I see status message "Confirm Timed Transcript" - And I see button "use_existing" - - And I enter a "video_name_3.webm" source to field number 2 - Then I see status message "Confirm Timed Transcript" - And I see button "use_existing" - And I click transcript button "use_existing" - And I see status message "Timed Transcript Found" - - I save video component And see that the captions are visible - Then I edit video component And I see status message "Timed Transcript Found" - """ - self.metadata = {'sub': 't_not_exist'} - self._create_video_component(subtitles=True, subtitle_id='t_not_exist') - self.edit_component() - - self.video.set_url_field('t_not_exist.mp4', 1) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.assertTrue(self.video.is_transcript_button_visible('download_to_edit')) - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - self.save_unit_settings() - self.edit_component() - - self.video.set_url_field('video_name_2.mp4', 1) - self.assertEqual(self.video.message('status'), 'Confirm Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('use_existing')) - - self.video.set_url_field('video_name_3.webm', 2) - self.assertEqual(self.video.message('status'), 'Confirm Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('use_existing')) - self.video.click_button('use_existing') - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - - self.save_unit_settings() - self.video.is_captions_visible() - - self.edit_component() - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - - def test_video_wo_subtitles(self): - """ - Scenario: Video w/o subs - another video w/o subs - Not found message - Video can have filled item.sub, but doesn't have subs file. - In this case, after changing this video by another one without subs - `No Timed Transcript` message should appear ( not 'Confirm Timed Transcript'). - Given I have created a Video component - - And I enter a "video_name_1.mp4" source to field number 1 - Then I see status message "No Timed Transcript" - """ - self._create_video_component() - self.edit_component() - - self.video.set_url_field('video_name_1.mp4', 1) - self.assertEqual(self.video.message('status'), 'No Timed Transcript') - - def test_upload_button_w_youtube(self): - """ - Scenario: Upload button for single youtube id - Given I have created a Video component - - After I enter a "http://youtu.be/t_not_exist" source to field number 1 I see message "No Timed Transcript" - And I see button "upload_new_timed_transcripts" - After I upload the transcripts file "uk_transcripts.srt" I see message "Timed Transcript Uploaded Successfully" - After saving the changes video captions should be visible - When I edit the component Then I see status message "Timed Transcript Found" - """ - self._create_video_component() - self.edit_component() - - self.video.set_url_field('http://youtu.be/t_not_exist', 1) - self.assertEqual(self.video.message('status'), 'No Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - self.video.upload_transcript('uk_transcripts.srt') - self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - - self.edit_component() - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - - def test_upload_button_w_html5_ids(self): - """ - Scenario: Upload button for youtube id with html5 ids - Given I have created a Video component - - After I enter a "http://youtu.be/t_not_exist" source to field number 1 I see message "No Timed Transcript" - And I see button "upload_new_timed_transcripts" - - After I enter a "video_name_1.mp4" source to field number 2 Then I see status message "No Timed Transcript" - And I see button "upload_new_timed_transcripts" - After I upload the transcripts file "uk_transcripts.srt"I see message "Timed Transcript Uploaded Successfully" - When I clear field number 1 Then I see status message "Timed Transcript Found" - After saving the changes video captions are visible - When I edit the component Then I see status message "Timed Transcript Found" - """ - self._create_video_component() - self.edit_component() - - self.video.set_url_field('http://youtu.be/t_not_exist', 1) - self.assertEqual(self.video.message('status'), 'No Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - - self.video.set_url_field('video_name_1.mp4', 2) - self.assertEqual(self.video.message('status'), 'No Timed Transcript') - self.assertTrue(self.video.is_transcript_button_visible('upload_new_timed_transcripts')) - self.video.upload_transcript('uk_transcripts.srt') - self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully') - # Removing a source from "Video URL" field will make an ajax call to `check_transcripts`. - self.video.clear_field(1) - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - - self.edit_component() - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - - def test_upload_subtitles_w_different_names3(self): - """ - Scenario: Shortened link: Shortened link to the source does not effect the uploaded - transcript, given I have created a Video component - - After I enter a "http://goo.gl/pxxZrg" source to field number 1 Then I see status message "No Timed Transcript" - After I upload the transcripts file "uk_transcripts.srt" I see message "Timed Transcript Uploaded Successfully" - After saving the changes video captions should be visible - After I edit the component I should see status message "Timed Transcript Found" - """ - self._create_video_component() - self.edit_component() - - self.video.set_url_field('http://goo.gl/pxxZrg', 1) - self.assertEqual(self.video.message('status'), 'No Timed Transcript') - self.video.upload_transcript('uk_transcripts.srt') - self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - - self.edit_component() - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') - - def test_upload_subtitles_w_different_names4(self): - """ - Scenario: Relative link: Relative link to the source does not effect the uploaded - transcript, given I have created a Video component - - After i enter a "/gizmo.webm" source to field number 1 Then I see status message "No Timed Transcript" - After I upload the transcripts file "uk_transcripts.srt" I see message "Timed Transcript Uploaded Successfully" - After saving the changes video captions should be visible - After I edit the component I should see status message "Timed Transcript Found" - """ - self._create_video_component() - self.edit_component() - - self.video.set_url_field('/gizmo.webm', 1) - self.assertEqual(self.video.message('status'), 'No Timed Transcript') - self.video.upload_transcript('uk_transcripts.srt') - self.assertEqual(self.video.message('status'), 'Timed Transcript Uploaded Successfully') - self.save_unit_settings() - self.assertTrue(self.video.is_captions_visible()) - - self.edit_component() - self.assertEqual(self.video.message('status'), 'Timed Transcript Found') diff --git a/common/test/acceptance/tests/video/test_video_events.py b/common/test/acceptance/tests/video/test_video_events.py deleted file mode 100644 index 1a1b21d9b6..0000000000 --- a/common/test/acceptance/tests/video/test_video_events.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Ensure videos emit proper events""" - - -import datetime -import json - -import six -from opaque_keys.edx.keys import CourseKey, UsageKey - -from common.test.acceptance.tests.helpers import EventsTestMixin -from common.test.acceptance.tests.video.test_video_module import VideoBaseTest -from openedx.core.lib.tests.assertions.events import assert_event_matches, assert_events_equal - - -class VideoEventsTestMixin(EventsTestMixin, VideoBaseTest): - """ - Useful helper methods to test video player event emission. - """ - def assert_payload_contains_ids(self, video_event): - """ - Video events should all contain "id" and "code" attributes in their payload. - - This function asserts that those fields are present and have correct values. - """ - video_descriptors = self.course_fixture.get_nested_xblocks(category='video') - video_desc = video_descriptors[0] - video_locator = UsageKey.from_string(video_desc.locator) - - expected_event = { - 'event': { - 'id': video_locator.html_id(), - 'code': '3_yD_cEKoCk' - } - } - self.assert_events_match([expected_event], [video_event]) - - def assert_valid_control_event_at_time(self, video_event, time_in_seconds): - """ - Video control events should contain valid ID fields and a valid "currentTime" field. - - This function asserts that those fields are present and have correct values. - """ - current_time = json.loads(video_event['event'])['currentTime'] - self.assertAlmostEqual(current_time, time_in_seconds, delta=1) - - def assert_field_type(self, event_dict, field, field_type): - """Assert that a particular `field` in the `event_dict` has a particular type""" - self.assertIn(field, event_dict, u'{0} not found in the root of the event'.format(field)) - self.assertTrue( - isinstance(event_dict[field], field_type), - u'Expected "{key}" to be a "{field_type}", but it has the value "{value}" of type "{t}"'.format( - key=field, - value=event_dict[field], - t=type(event_dict[field]), - field_type=field_type, - ) - ) - - -class VideoEventsTest(VideoEventsTestMixin): - """ Test video player event emission """ - shard = 21 - - def test_video_control_events(self): - """ - Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources - Given the course has a Video component in "Youtube" mode - And I play the video - And I watch 5 seconds of it - And I pause the video - Then a "load_video" event is emitted - And a "play_video" event is emitted - And a "pause_video" event is emitted - """ - - def is_video_event(event): - """Filter out anything other than the video events of interest""" - return event['event_type'] in ('load_video', 'play_video', 'pause_video') - - captured_events = [] - with self.capture_events(is_video_event, number_of_matches=3, captured_events=captured_events): - self.navigate_to_video() - self.video.click_player_button('play') - self.video.wait_for_position('0:05') - self.video.click_player_button('pause') - - for idx, video_event in enumerate(captured_events): - self.assert_payload_contains_ids(video_event) - if idx == 0: - assert_event_matches({'event_type': 'load_video'}, video_event) - elif idx == 1: - assert_event_matches({'event_type': 'play_video'}, video_event) - self.assert_valid_control_event_at_time(video_event, 0) - elif idx == 2: - assert_event_matches({'event_type': 'pause_video'}, video_event) - self.assert_valid_control_event_at_time(video_event, self.video.seconds) - - def test_strict_event_format(self): - """ - This test makes a very strong assertion about the fields present in events. The goal of it is to ensure that new - fields are not added to all events mistakenly. It should be the only existing test that is updated when new top - level fields are added to all events. - """ - - captured_events = [] - with self.capture_events(lambda e: e['event_type'] == 'load_video', captured_events=captured_events): - self.navigate_to_video() - - load_video_event = captured_events[0] - - # Validate the event payload - self.assert_payload_contains_ids(load_video_event) - - # We cannot predict the value of these fields so we make weaker assertions about them - dynamic_string_fields = ( - 'accept_language', - 'agent', - 'host', - 'ip', - 'event', - 'session' - ) - for field in dynamic_string_fields: - self.assert_field_type(load_video_event, field, six.string_types) - self.assertIn(field, load_video_event, u'{0} not found in the root of the event'.format(field)) - del load_video_event[field] - - # A weak assertion for the timestamp as well - self.assert_field_type(load_video_event, 'time', datetime.datetime) - del load_video_event['time'] - - # Note that all unpredictable fields have been deleted from the event at this point - - course_key = CourseKey.from_string(self.course_id) - static_fields_pattern = { - 'context': { - 'course_id': six.text_type(course_key), - 'org_id': course_key.org, - 'path': '/event', - 'user_id': self.user_info['user_id'] - }, - 'event_source': 'browser', - 'event_type': 'load_video', - 'username': self.user_info['username'], - 'page': self.browser.current_url, - 'referer': self.browser.current_url, - 'name': 'load_video', - } - assert_events_equal(static_fields_pattern, load_video_event) - - -class VideoHLSEventsTest(VideoEventsTestMixin): - """ - Test video player event emission for HLS video - """ - shard = 19 - - def test_event_data_for_hls(self): - """ - Scenario: Video component with HLS video emits events correctly - - Given the course has a Video component with Youtube, HTML5 and HLS sources available. - And I play the video - And the video starts playing - And I watch 3 seconds of it - When I pause and seek the video - And I play the video to the end - Then I verify that all expected events are triggered - And triggered events have correct data - """ - video_events = ('load_video', 'play_video', 'pause_video', 'seek_video') - - def is_video_event(event): - """ - Filter out anything other than the video events of interest - """ - return event['event_type'] in video_events - - captured_events = [] - with self.capture_events(is_video_event, captured_events=captured_events): - self.metadata = self.metadata_for_mode('hls') - self.navigate_to_video() - self.video.click_player_button('play') - self.video.wait_for_position('0:03') - self.video.click_player_button('pause') - self.video.seek('0:08') - - expected_events = [{'name': event, 'event': {'code': 'hls'}} for event in video_events] - self.assert_events_match(expected_events, captured_events) diff --git a/common/test/acceptance/tests/video/test_video_module.py b/common/test/acceptance/tests/video/test_video_module.py index d868201527..e0d889ad53 100644 --- a/common/test/acceptance/tests/video/test_video_module.py +++ b/common/test/acceptance/tests/video/test_video_module.py @@ -7,14 +7,10 @@ Acceptance tests for Video. import os from unittest import skipIf - -from ddt import data, ddt, unpack from mock import patch -from six.moves import range from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.pages.common.auto_auth import AutoAuthPage -from common.test.acceptance.pages.lms.course_info import CourseInfoPage from common.test.acceptance.pages.lms.courseware import CoursewarePage from common.test.acceptance.pages.lms.tab_nav import TabNavPage from common.test.acceptance.pages.lms.video.video import VideoPage @@ -22,7 +18,6 @@ from common.test.acceptance.tests.helpers import ( UniqueCourseTest, YouTubeStubConfig, is_youtube_available, - skip_if_browser ) from openedx.core.lib.tests import attr @@ -61,7 +56,6 @@ class VideoBaseTest(UniqueCourseTest): self.video = VideoPage(self.browser) self.tab_nav = TabNavPage(self.browser) self.courseware_page = CoursewarePage(self.browser, self.course_id) - self.course_info_page = CourseInfoPage(self.browser, self.course_id) self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id) self.course_fixture = CourseFixture( @@ -217,727 +211,6 @@ class VideoBaseTest(UniqueCourseTest): self.video.wait_for_video_player_render() -@attr(shard=13) -@ddt -class YouTubeVideoTest(VideoBaseTest): - """ Test YouTube Video Player """ - - def test_youtube_video_rendering_wo_html5_sources(self): - """ - Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources - Given the course has a Video component in "Youtube" mode - Then the video has rendered in "Youtube" mode - """ - self.navigate_to_video() - - # Verify that video has rendered in "Youtube" mode - self.assertTrue(self.video.is_video_rendered('youtube')) - - def test_transcript_button_wo_english_transcript(self): - """ - Scenario: Transcript button works correctly w/o english transcript in Youtube mode - Given the course has a Video component in "Youtube" mode - And I have defined a non-english transcript for the video - And I have uploaded a non-english transcript file to assets - Then I see the correct text in the captions - """ - data = {'transcripts': {'zh': 'chinese_transcripts.srt'}} - self.metadata = self.metadata_for_mode('youtube', data) - self.assets.append('chinese_transcripts.srt') - self.navigate_to_video() - self.video.show_captions() - - # Verify that we see "好 各位同学" text in the transcript - unicode_text = u"好 各位同学" - self.assertIn(unicode_text, self.video.captions_text) - - def test_cc_button(self): - """ - Scenario: CC button works correctly with transcript in YouTube mode - Given the course has a video component in "Youtube" mode - And I have defined a transcript for the video - Then I see the closed captioning element over the video - """ - data = {'transcripts': {'zh': 'chinese_transcripts.srt'}} - self.metadata = self.metadata_for_mode('youtube', data) - self.assets.append('chinese_transcripts.srt') - self.navigate_to_video() - - # Show captions and make sure they're visible and cookie is set - self.video.show_closed_captions() - self.video.wait_for_closed_captions() - self.assertTrue(self.video.is_closed_captions_visible) - self.video.reload_page() - self.assertTrue(self.video.is_closed_captions_visible) - - # Hide captions and make sure they're hidden and cookie is unset - self.video.hide_closed_captions() - self.video.wait_for_closed_captions_to_be_hidden() - self.video.reload_page() - self.video.wait_for_closed_captions_to_be_hidden() - - def test_transcript_button_transcripts_and_sub_fields_empty(self): - """ - Scenario: Transcript button works correctly if transcripts and sub fields are empty, - but transcript file exists in assets (Youtube mode of Video component) - Given the course has a Video component in "Youtube" mode - And I have uploaded a .srt.sjson file to assets - Then I see the correct english text in the captions - """ - self._install_course_fixture() - self.course_fixture.add_asset(['subs_3_yD_cEKoCk.srt.sjson']) - self.course_fixture._upload_assets() - self._navigate_to_courseware_video_and_render() - self.video.show_captions() - - # Verify that we see "Welcome to edX." text in the captions - self.assertIn('Welcome to edX.', self.video.captions_text) - - def test_transcript_button_hidden_no_translations(self): - """ - Scenario: Transcript button is hidden if no translations - Given the course has a Video component in "Youtube" mode - Then the "Transcript" button is hidden - """ - self.navigate_to_video() - self.assertFalse(self.video.is_button_shown('transcript_button')) - - def test_download_button_wo_english_transcript(self): - """ - Scenario: Download button works correctly w/o english transcript in YouTube mode - Given the course has a Video component in "Youtube" mode - And I have defined a downloadable non-english transcript for the video - And I have uploaded a non-english transcript file to assets - Then I can download the transcript in "srt" format - """ - data = {'download_track': True, 'transcripts': {'zh': 'chinese_transcripts.srt'}} - self.metadata = self.metadata_for_mode('youtube', additional_data=data) - self.assets.append('chinese_transcripts.srt') - - # go to video - self.navigate_to_video() - - # check if we can download transcript in "srt" format that has text "好 各位同学" - unicode_text = u"好 各位同学" - self.assertTrue(self.video.downloaded_transcript_contains_text('srt', unicode_text)) - - def test_download_button_two_transcript_languages(self): - """ - Scenario: Download button works correctly for multiple transcript languages - Given the course has a Video component in "Youtube" mode - And I have defined a downloadable non-english transcript for the video - And I have defined english subtitles for the video - Then I see the correct english text in the captions - And the english transcript downloads correctly - And I see the correct non-english text in the captions - And the non-english transcript downloads correctly - """ - self.assets.extend(['chinese_transcripts.srt', 'subs_3_yD_cEKoCk.srt.sjson']) - data = {'download_track': True, 'transcripts': {'zh': 'chinese_transcripts.srt'}, 'sub': '3_yD_cEKoCk'} - self.metadata = self.metadata_for_mode('youtube', additional_data=data) - - # go to video - self.navigate_to_video() - - # check if "Welcome to edX." text in the captions - self.assertIn('Welcome to edX.', self.video.captions_text) - - # check if we can download transcript in "srt" format that has text "Welcome to edX." - self.assertTrue(self.video.downloaded_transcript_contains_text('srt', 'Welcome to edX.')) - - # select language with code "zh" - self.assertTrue(self.video.select_language('zh')) - - # check if we see "好 各位同学" text in the captions - unicode_text = u"好 各位同学" - self.assertIn(unicode_text, self.video.captions_text) - - # check if we can download transcript in "srt" format that has text "好 各位同学" - unicode_text = u"好 各位同学" - self.assertTrue(self.video.downloaded_transcript_contains_text('srt', unicode_text)) - - def test_video_rendering_with_default_response_time(self): - """ - Scenario: Video is rendered in Youtube mode when the YouTube Server responds quickly - Given the YouTube server response time less than 1.5 seconds - And the course has a Video component in "Youtube_HTML5" mode - Then the video has rendered in "Youtube" mode - """ - # configure youtube server - self.youtube_configuration['time_to_response'] = 0.4 - self.metadata = self.metadata_for_mode('youtube_html5') - - self.navigate_to_video() - - self.assertTrue(self.video.is_video_rendered('youtube')) - - def test_video_rendering_wo_default_response_time(self): - """ - Scenario: Video is rendered in HTML5 when the YouTube Server responds slowly - Given the YouTube server response time is greater than 1.5 seconds - And the course has a Video component in "Youtube_HTML5" mode - Then the video has rendered in "HTML5" mode - """ - # configure youtube server - self.youtube_configuration['time_to_response'] = 7.0 - self.metadata = self.metadata_for_mode('youtube_html5') - - self.navigate_to_video() - - self.assertTrue(self.video.is_video_rendered('html5')) - - def test_video_with_youtube_blocked_with_default_response_time(self): - """ - Scenario: Video is rendered in HTML5 mode when the YouTube API is blocked - Given the YouTube API is blocked - And the course has a Video component in "Youtube_HTML5" mode - Then the video has rendered in "HTML5" mode - And only one video has rendered - """ - # configure youtube server - self.youtube_configuration.update({ - 'youtube_api_blocked': True, - }) - - self.metadata = self.metadata_for_mode('youtube_html5') - - self.navigate_to_video() - - self.assertTrue(self.video.is_video_rendered('html5')) - - # The video should only be loaded once - self.assertEqual(len(self.video.q(css='video')), 1) - - def test_video_with_youtube_blocked_delayed_response_time(self): - """ - Scenario: Video is rendered in HTML5 mode when the YouTube API is blocked - Given the YouTube server response time is greater than 1.5 seconds - And the YouTube API is blocked - And the course has a Video component in "Youtube_HTML5" mode - Then the video has rendered in "HTML5" mode - And only one video has rendered - """ - # configure youtube server - self.youtube_configuration.update({ - 'time_to_response': 2.0, - 'youtube_api_blocked': True, - }) - - self.metadata = self.metadata_for_mode('youtube_html5') - - self.navigate_to_video() - - self.assertTrue(self.video.is_video_rendered('html5')) - - # The video should only be loaded once - self.assertEqual(len(self.video.q(css='video')), 1) - - def test_html5_video_rendered_with_youtube_captions(self): - """ - Scenario: User should see Youtube captions for If there are no transcripts - available for HTML5 mode - Given that I have uploaded a .srt.sjson file to assets for Youtube mode - And the YouTube API is blocked - And the course has a Video component in "Youtube_HTML5" mode - And Video component rendered in HTML5 mode - And Html5 mode video has no transcripts - When I see the captions for HTML5 mode video - Then I should see the Youtube captions - """ - self.assets.append('subs_3_yD_cEKoCk.srt.sjson') - # configure youtube server - self.youtube_configuration.update({ - 'time_to_response': 2.0, - 'youtube_api_blocked': True, - }) - - data = {'sub': '3_yD_cEKoCk'} - self.metadata = self.metadata_for_mode('youtube_html5', additional_data=data) - - self.navigate_to_video() - - self.assertTrue(self.video.is_video_rendered('html5')) - # check if caption button is visible - self.assertTrue(self.video.is_button_shown('transcript_button')) - self._verify_caption_text('Welcome to edX.') - - @data(('srt', '00:00:00,260'), ('txt', 'Welcome to edX.')) - @unpack - def test_download_transcript_links_work_correctly(self, file_type, search_text): - """ - Scenario: Download 'srt' transcript link works correctly. - Download 'txt' transcript link works correctly. - Given the course has Video components A and B in "Youtube" mode - And Video component C in "HTML5" mode - And I have defined downloadable transcripts for the videos - Then I can download a transcript for Video A in "srt" format - And the Download Transcript menu does not exist for Video C - """ - - data_a = {'sub': '3_yD_cEKoCk', 'download_track': True} - youtube_a_metadata = self.metadata_for_mode('youtube', additional_data=data_a) - self.assets.append('subs_3_yD_cEKoCk.srt.sjson') - - data_b = {'youtube_id_1_0': 'b7xgknqkQk8', 'sub': 'b7xgknqkQk8', 'download_track': True} - youtube_b_metadata = self.metadata_for_mode('youtube', additional_data=data_b) - self.assets.append('subs_b7xgknqkQk8.srt.sjson') - - data_c = {'track': 'http://example.org/', 'download_track': True} - html5_c_metadata = self.metadata_for_mode('html5', additional_data=data_c) - - self.contents_of_verticals = [ - [{'display_name': 'A', 'metadata': youtube_a_metadata}], - [{'display_name': 'B', 'metadata': youtube_b_metadata}], - [{'display_name': 'C', 'metadata': html5_c_metadata}] - ] - - # open the section with videos (open vertical containing video "A") - self.navigate_to_video() - - # check if we can download transcript in "srt" format that has text "00:00:00,260" - self.assertTrue(self.video.downloaded_transcript_contains_text(file_type, search_text)) - - # open vertical containing video "C" - self.courseware_page.nav.go_to_vertical('Test Vertical-2') - - # menu "download_transcript" doesn't exist - self.assertFalse(self.video.is_menu_present('download_transcript')) - - def _verify_caption_text(self, text): - self.video._wait_for( - lambda: (text in self.video.captions_text), - u'Captions contain "{}" text'.format(text), - timeout=5 - ) - - def _verify_closed_caption_text(self, text): - """ - Scenario: returns True if the captions are visible, False is else - """ - self.video.wait_for( - lambda: (text in self.video.closed_captions_text), - u'Closed captions contain "{}" text'.format(text), - timeout=5 - ) - - def test_video_language_menu_working_closed_captions(self): - """ - Scenario: Language menu works correctly in Video component, checks closed captions - Given the course has a Video component in "Youtube" mode - And I have defined multiple language transcripts for the videos - And I make sure captions are closed - And I see video menu "language" with correct items - And I select language with code "en" - Then I see "Welcome to edX." text in the closed captions - And I select language with code "zh" - Then I see "我们今天要讲的题目是" text in the closed captions - """ - self.assets.extend(['chinese_transcripts.srt', 'subs_3_yD_cEKoCk.srt.sjson']) - data = {'transcripts': {"zh": "chinese_transcripts.srt"}, 'sub': '3_yD_cEKoCk'} - self.metadata = self.metadata_for_mode('youtube', additional_data=data) - - # go to video - self.navigate_to_video() - self.video.show_closed_captions() - - correct_languages = {'en': 'English', 'zh': 'Chinese'} - self.assertEqual(self.video.caption_languages, correct_languages) - - # we start the video, then pause it to activate the transcript - self.video.click_player_button('play') - self.video.wait_for_position('0:03') - self.video.click_player_button('pause') - - self.video.select_language('en') - self.video.click_transcript_line(line_no=1) - self._verify_closed_caption_text('Welcome to edX.') - - self.video.select_language('zh') - unicode_text = u"我们今天要讲的题目是" - self.video.click_transcript_line(line_no=1) - self._verify_closed_caption_text(unicode_text) - - def test_video_component_stores_speed_correctly_for_multiple_videos(self): - """ - Scenario: Video component stores speed correctly when each video is in separate sequential - Given I have a video "A" in "Youtube" mode in position "1" of sequential - And a video "B" in "Youtube" mode in position "2" of sequential - And a video "C" in "HTML5" mode in position "3" of sequential - """ - # vertical titles are created in VideoBaseTest._create_single_vertical - # and are of the form Test Vertical-{_} where _ is the index in self.contents_of_verticals - self.contents_of_verticals = [ - [{'display_name': 'A'}], [{'display_name': 'B'}], - [{'display_name': 'C', 'metadata': self.metadata_for_mode('html5')}] - ] - - self.navigate_to_video() - - # select the "2.0" speed on video "A" - self.courseware_page.nav.go_to_vertical('Test Vertical-0') - self.video.wait_for_video_player_render() - self.video.speed = '2.0' - - # select the "0.50" speed on video "B" - self.courseware_page.nav.go_to_vertical('Test Vertical-1') - self.video.wait_for_video_player_render() - self.video.speed = '0.50' - - # open video "C" - self.courseware_page.nav.go_to_vertical('Test Vertical-2') - self.video.wait_for_video_player_render() - - # Since the playback speed was set to .5 in "B", this video will also be impacted - # because a playback speed has never explicitly been set for it. However, this video - # does not have a .5 playback option, so the closest possible (.75) should be selected. - self.video.verify_speed_changed('0.75x') - - # go to the vertical containing video "A" - self.courseware_page.nav.go_to_vertical('Test Vertical-0') - - # Video "A" should still play at speed 2.0 because it was explicitly set to that. - self.assertEqual(self.video.speed, '2.0x') - - # reload the page - self.video.reload_page() - - # go to the vertical containing video "A" - self.courseware_page.nav.go_to_vertical('Test Vertical-0') - - # check if video "A" should start playing at speed "2.0" - self.assertEqual(self.video.speed, '2.0x') - - # select the "1.0" speed on video "A" - self.video.speed = '1.0' - - # go to the vertical containing "B" - self.courseware_page.nav.go_to_vertical('Test Vertical-1') - - # Video "B" should still play at speed .5 because it was explicitly set to that. - self.assertEqual(self.video.speed, '0.50x') - - # go to the vertical containing video "C" - self.courseware_page.nav.go_to_vertical('Test Vertical-2') - - # The change of speed for Video "A" should impact Video "C" because it still has - # not been explicitly set to a speed. - self.video.verify_speed_changed('1.0x') - - def test_video_has_correct_transcript(self): - """ - Scenario: Youtube video has correct transcript if fields for other speeds are filled - Given it has a video in "Youtube" mode - And I have uploaded multiple transcripts - And I make sure captions are opened - Then I see "Welcome to edX." text in the captions - And I select the "1.50" speed - And I reload the page with video - Then I see "Welcome to edX." text in the captions - And I see duration "1:56" - - """ - self.assets.extend(['subs_3_yD_cEKoCk.srt.sjson', 'subs_b7xgknqkQk8.srt.sjson']) - data = {'sub': '3_yD_cEKoCk', 'youtube_id_1_5': 'b7xgknqkQk8'} - self.metadata = self.metadata_for_mode('youtube', additional_data=data) - - # go to video - self.navigate_to_video() - - self.video.show_captions() - - self.assertIn('Welcome to edX.', self.video.captions_text) - - self.video.speed = '1.50' - - self.video.reload_page() - - self.assertIn('Welcome to edX.', self.video.captions_text) - - self.assertTrue(self.video.duration, '1.56') - - def test_video_position_stored_correctly_wo_seek(self): - """ - Scenario: Video component stores position correctly when page is reloaded - Given the course has a Video component in "Youtube" mode - Then the video has rendered in "Youtube" mode - And I click video button "play"" - Then I wait until video reaches at position "0.03" - And I click video button "pause" - And I reload the page with video - And I click video button "play"" - And I click video button "pause" - Then video slider should be Equal or Greater than "0:03" - - """ - self.navigate_to_video() - - self.video.click_player_button('play') - - self.video.wait_for_position('0:03') - - self.video.click_player_button('pause') - - self.video.reload_page() - - self.video.click_player_button('play') - self.video.click_player_button('pause') - - self.assertGreaterEqual(self.video.seconds, 3) - - def test_simplified_and_traditional_chinese_transcripts(self): - """ - Scenario: Simplified and Traditional Chinese transcripts work as expected in Youtube mode - - Given the course has a Video component in "Youtube" mode - And I have defined a Simplified Chinese transcript for the video - And I have defined a Traditional Chinese transcript for the video - Then I see the correct subtitle language options in cc menu - Then I see the correct text in the captions for Simplified and Traditional Chinese transcripts - And I can download the transcripts for Simplified and Traditional Chinese - And video subtitle menu has 'zh_HANS', 'zh_HANT' translations for 'Simplified Chinese' - and 'Traditional Chinese' respectively - """ - data = { - 'download_track': True, - 'transcripts': {'zh_HANS': 'simplified_chinese.srt', 'zh_HANT': 'traditional_chinese.srt'} - } - self.metadata = self.metadata_for_mode('youtube', data) - self.assets.extend(['simplified_chinese.srt', 'traditional_chinese.srt']) - self.navigate_to_video() - - langs = {'zh_HANS': u'在线学习是革', 'zh_HANT': u'在線學習是革'} - for lang_code, unicode_text in langs.items(): - self.video.scroll_to_button("transcript_button") - self.assertTrue(self.video.select_language(lang_code)) - self.assertIn(unicode_text, self.video.captions_text) - self.assertTrue(self.video.downloaded_transcript_contains_text('srt', unicode_text)) - - self.assertEqual(self.video.caption_languages, {'zh_HANS': 'Simplified Chinese', 'zh_HANT': 'Traditional Chinese'}) - - -@attr(shard=13) -class YouTubeHtml5VideoTest(VideoBaseTest): - """ Test YouTube HTML5 Video Player """ - - def test_youtube_video_rendering_with_unsupported_sources(self): - """ - Scenario: Video component is rendered in the LMS in Youtube mode - with HTML5 sources that doesn't supported by browser - Given the course has a Video component in "Youtube_HTML5_Unsupported_Video" mode - Then the video has rendered in "Youtube" mode - """ - self.metadata = self.metadata_for_mode('youtube_html5_unsupported_video') - self.navigate_to_video() - - # Verify that the video has rendered in "Youtube" mode - self.assertTrue(self.video.is_video_rendered('youtube')) - - -@attr(shard=19) -class Html5VideoTest(VideoBaseTest): - """ Test HTML5 Video Player """ - - def test_autoplay_disabled_for_video_component(self): - """ - Scenario: Autoplay is disabled by default for a Video component - Given the course has a Video component in "HTML5" mode - When I view the Video component - Then it does not have autoplay enabled - """ - self.metadata = self.metadata_for_mode('html5') - self.navigate_to_video() - - # Verify that the video has autoplay mode disabled - self.assertFalse(self.video.is_autoplay_enabled) - - def test_html5_video_rendering_with_unsupported_sources(self): - """ - Scenario: LMS displays an error message for HTML5 sources that are not supported by browser - Given the course has a Video component in "HTML5_Unsupported_Video" mode - When I view the Video component - Then and error message is shown - And the error message has the correct text - """ - self.metadata = self.metadata_for_mode('html5_unsupported_video') - self.navigate_to_video_no_render() - - # Verify that error message is shown - self.assertTrue(self.video.is_error_message_shown) - - # Verify that error message has correct text - correct_error_message_text = 'No playable video sources found.' - self.assertIn(correct_error_message_text, self.video.error_message_text) - - # Verify that spinner is not shown - self.assertFalse(self.video.is_spinner_shown) - - def test_download_button_wo_english_transcript(self): - """ - Scenario: Download button works correctly w/o english transcript in HTML5 mode - Given the course has a Video component in "HTML5" mode - And I have defined a downloadable non-english transcript for the video - And I have uploaded a non-english transcript file to assets - Then I see the correct non-english text in the captions - And the non-english transcript downloads correctly - """ - data = {'download_track': True, 'transcripts': {'zh': 'chinese_transcripts.srt'}} - self.metadata = self.metadata_for_mode('html5', additional_data=data) - self.assets.append('chinese_transcripts.srt') - - # go to video - self.navigate_to_video() - - # check if we see "好 各位同学" text in the captions - unicode_text = u"好 各位同学" - self.assertIn(unicode_text, self.video.captions_text) - - # check if we can download transcript in "srt" format that has text "好 各位同学" - unicode_text = u"好 各位同学" - self.assertTrue(self.video.downloaded_transcript_contains_text('srt', unicode_text)) - - def test_download_button_two_transcript_languages(self): - """ - Scenario: Download button works correctly for multiple transcript languages in HTML5 mode - Given the course has a Video component in "HTML5" mode - And I have defined a downloadable non-english transcript for the video - And I have defined english subtitles for the video - Then I see the correct english text in the captions - And the english transcript downloads correctly - And I see the correct non-english text in the captions - And the non-english transcript downloads correctly - """ - self.assets.extend(['chinese_transcripts.srt', 'subs_3_yD_cEKoCk.srt.sjson']) - data = {'download_track': True, 'transcripts': {'zh': 'chinese_transcripts.srt'}, 'sub': '3_yD_cEKoCk'} - self.metadata = self.metadata_for_mode('html5', additional_data=data) - - # go to video - self.navigate_to_video() - - # check if "Welcome to edX." text in the captions - self.assertIn('Welcome to edX.', self.video.captions_text) - - self.video.wait_for_element_visibility('.transcript-end', 'Transcript has loaded') - - # check if we can download transcript in "srt" format that has text "Welcome to edX." - self.assertTrue(self.video.downloaded_transcript_contains_text('srt', 'Welcome to edX.')) - - # select language with code "zh" - self.assertTrue(self.video.select_language('zh')) - - # check if we see "好 各位同学" text in the captions - unicode_text = u"好 各位同学" - - self.assertIn(unicode_text, self.video.captions_text) - - # Then I can download transcript in "srt" format that has text "好 各位同学" - unicode_text = u"好 各位同学" - self.assertTrue(self.video.downloaded_transcript_contains_text('srt', unicode_text)) - - def test_cc_button_with_english_transcript(self): - """ - Scenario: CC button works correctly with only english transcript in HTML5 mode - Given the course has a Video component in "HTML5" mode - And I have defined english subtitles for the video - And I have uploaded an english transcript file to assets - Then I see the correct text in the captions - """ - self.assets.append('subs_3_yD_cEKoCk.srt.sjson') - data = {'sub': '3_yD_cEKoCk'} - self.metadata = self.metadata_for_mode('html5', additional_data=data) - - # go to video - self.navigate_to_video() - - # make sure captions are opened - self.video.show_captions() - - # check if we see "Welcome to edX." text in the captions - self.assertIn("Welcome to edX.", self.video.captions_text) - - def test_cc_button_wo_english_transcript(self): - """ - Scenario: CC button works correctly w/o english transcript in HTML5 mode - Given the course has a Video component in "HTML5" mode - And I have defined a non-english transcript for the video - And I have uploaded a non-english transcript file to assets - Then I see the correct text in the captions - """ - self.assets.append('chinese_transcripts.srt') - data = {'transcripts': {'zh': 'chinese_transcripts.srt'}} - self.metadata = self.metadata_for_mode('html5', additional_data=data) - - # go to video - self.navigate_to_video() - - # make sure captions are opened - self.video.show_captions() - - # check if we see "好 各位同学" text in the captions - unicode_text = u"好 各位同学" - self.assertIn(unicode_text, self.video.captions_text) - - def test_video_rendering(self): - """ - Scenario: Video component is fully rendered in the LMS in HTML5 mode - Given the course has a Video component in "HTML5" mode - Then the video has rendered in "HTML5" mode - And video sources are correct - """ - self.metadata = self.metadata_for_mode('html5') - - self.navigate_to_video() - - self.assertTrue(self.video.is_video_rendered('html5')) - - self.assertTrue(all([source in HTML5_SOURCES for source in self.video.sources])) - - -@attr(shard=13) -class YouTubeQualityTest(VideoBaseTest): - """ Test YouTube Video Quality Button """ - - @skip_if_browser('firefox') - def test_quality_button_visibility(self): - """ - Scenario: Quality button appears on play. - - Given the course has a Video component in "Youtube" mode - Then I see video button "quality" is hidden - And I click video button "play" - Then I see video button "quality" is visible - """ - self.navigate_to_video() - - self.assertFalse(self.video.is_quality_button_visible) - - self.video.click_player_button('play') - - self.video.wait_for(lambda: self.video.is_quality_button_visible, 'waiting for quality button to appear') - - @skip_if_browser('firefox') - def test_quality_button_works_correctly(self): - """ - Scenario: Quality button works correctly. - - Given the course has a Video component in "Youtube" mode - And I click video button "play" - And I see video button "quality" is inactive - And I click video button "quality" - Then I see video button "quality" is active - """ - self.navigate_to_video() - - self.video.click_player_button('play') - - self.video.wait_for(lambda: self.video.is_quality_button_visible, 'waiting for quality button to appear') - - self.assertFalse(self.video.is_quality_button_active) - - self.video.click_player_button('quality') - - self.video.wait_for(lambda: self.video.is_quality_button_active, 'waiting for quality button activation') - - @attr('a11y') class LMSVideoBlockA11yTest(VideoBaseTest): """ @@ -973,166 +246,3 @@ class LMSVideoBlockA11yTest(VideoBaseTest): include=["div.video"] ) self.video.a11y_audit.check_for_accessibility_errors() - - -@attr(shard=11) -class VideoPlayOrderTest(VideoBaseTest): - """ - Test video play order with multiple videos - - Priority of video formats is: - * Youtube - * HLS - * HTML5 - """ - - def setUp(self): - super(VideoPlayOrderTest, self).setUp() - - def test_play_youtube_video(self): - """ - Scenario: Correct video is played when we have different video formats. - - Given the course has a Video component with Youtube, HTML5 and HLS sources available. - When I view the Video component - Then it should play the Youtube video - """ - additional_data = {'youtube_id_1_0': 'b7xgknqkQk8'} - self.metadata = self.metadata_for_mode('html5_and_hls', additional_data=additional_data) - self.navigate_to_video() - - # Verify that the video is youtube - self.assertTrue(self.video.is_video_rendered('youtube')) - - def test_play_html5_hls_video(self): - """ - Scenario: HLS video is played when we have HTML5 and HLS video formats only. - - Given the course has a Video component with HTML5 and HLS sources available. - When I view the Video component - Then it should play the HLS video - """ - self.metadata = self.metadata_for_mode('html5_and_hls') - self.navigate_to_video() - - # Verify that the video is hls - self.assertTrue(self.video.is_video_rendered('hls')) - - -@attr(shard=11) -class HLSVideoTest(VideoBaseTest): - """ - Tests related to HLS video - """ - - def test_video_play_pause(self): - """ - Scenario: Video play and pause is working as expected for hls video - - Given the course has a Video component with only HLS source available. - When I view the Video component - Then I can see play and pause are working as expected - """ - self.metadata = self.metadata_for_mode('hls') - self.navigate_to_video() - - self.video.click_player_button('play') - self.assertIn(self.video.state, ['buffering', 'playing']) - self.video.click_player_button('pause') - self.assertEqual(self.video.state, 'pause') - - def test_video_seek(self): - """ - Scenario: Video seek is working as expected for hls video - - Given the course has a Video component with only HLS source available. - When I view the Video component - Then I can seek the video as expected - """ - self.metadata = self.metadata_for_mode('hls') - self.navigate_to_video() - - self.video.click_player_button('play') - self.video.wait_for_position('0:02') - self.video.click_player_button('pause') - self.video.seek('0:05') - self.assertEqual(self.video.position, '0:05') - - def test_video_download_link(self): - """ - Scenario: Correct video url is selected for download - - Given the course has a Video component with Youtube, HTML5 and HLS sources available. - When I view the Video component - Then HTML5 video download url is available - """ - self.metadata = self.metadata_for_mode('html5_and_hls', additional_data={'download_video': True}) - self.navigate_to_video() - - # Verify that the video download url is correct - self.assertEqual(self.video.video_download_url, HTML5_SOURCES[0]) - - def test_no_video_download_link_for_hls(self): - """ - Scenario: Video download url is not shown for hls videos - - Given the course has a Video component with only HLS sources available. - When I view the Video component - Then there is no video download url shown - """ - additional_data = {'download_video': True} - self.metadata = self.metadata_for_mode('hls', additional_data=additional_data) - self.navigate_to_video() - - # Verify that the video download url is not shown - self.assertEqual(self.video.video_download_url, None) - - def test_hls_video_with_youtube_delayed_response_time(self): - """ - Scenario: HLS video is rendered when the YouTube API response time is slow - Given the YouTube server response time is greater than 1.5 seconds - And the course has a Video component with Youtube, HTML5 and HLS sources available - Then the HLS video is rendered - """ - # configure youtube server - self.youtube_configuration.update({ - 'time_to_response': 7.0, - }) - - self.metadata = self.metadata_for_mode('html5_and_hls', additional_data={'youtube_id_1_0': 'b7xgknqkQk8'}) - self.navigate_to_video() - self.assertTrue(self.video.is_video_rendered('hls')) - - def test_hls_video_with_transcript(self): - """ - Scenario: Transcript work as expected for an HLS video - - Given the course has a Video component with "HLS" video only - And I have defined a transcript for the video - Then I see the correct text in the captions for transcript - Then I play, pause and seek to 0:00 - Then I click on a caption line - And video position should be updated accordingly - Then I change video position - And video caption should be updated accordingly - """ - data = {'transcripts': {'zh': 'transcript.srt'}} - self.metadata = self.metadata_for_mode('hls', additional_data=data) - self.assets.append('transcript.srt') - self.navigate_to_video() - - self.assertIn("Hi, edX welcomes you0.", self.video.captions_text) - - # This is required to load the video - self.video.click_player_button('play') - # Below 2 steps are required to test the caption line click scenario - self.video.click_player_button('pause') - self.video.seek('0:00') - - for line_no in range(5): - self.video.click_transcript_line(line_no=line_no) - self.video.wait_for_position(u'0:0{}'.format(line_no)) - - for line_no in range(5): - self.video.seek(u'0:0{}'.format(line_no)) - self.assertEqual(self.video.active_caption_text, u'Hi, edX welcomes you{}.'.format(line_no)) diff --git a/common/test/acceptance/tests/video/test_video_times.py b/common/test/acceptance/tests/video/test_video_times.py deleted file mode 100644 index b641fe317d..0000000000 --- a/common/test/acceptance/tests/video/test_video_times.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Acceptance tests for Video Times(Start, End and Finish) functionality. -""" - - -from common.test.acceptance.tests.video.test_video_module import VideoBaseTest - - -class VideoTimesTest(VideoBaseTest): - """ Test Video Player Times """ - shard = 21 - - def test_video_start_time(self): - """ - Scenario: Start time works for Youtube video - Given we have a video in "Youtube" mode with start_time set to 00:00:10 - And I see video slider at "0:10" position - And I click video button "play" - Then video starts playing at or after start_time(00:00:10) - - """ - data = {'start_time': '00:00:10'} - self.metadata = self.metadata_for_mode('youtube', additional_data=data) - - # go to video - self.navigate_to_video() - - self.assertEqual(self.video.position, '0:10') - - self.video.click_player_button('play') - - self.assertGreaterEqual(int(self.video.position.split(':')[1]), 10) - - def test_video_end_time_with_default_start_time(self): - """ - Scenario: End time works for Youtube video if starts playing from beginning. - Given we have a video in "Youtube" mode with end time set to 00:00:05 - And I click video button "play" - And I wait until video stop playing - Then I see video slider at "0:05" position - - """ - data = {'end_time': '00:00:05'} - self.metadata = self.metadata_for_mode('youtube', additional_data=data) - - # go to video - self.navigate_to_video() - - self.video.click_player_button('play') - - # wait until video stop playing - self.video.wait_for_state('pause') - - self.assertIn(self.video.position, ('0:05', '0:06')) - - def test_video_start_time_and_end_time(self): - """ - Scenario: Start time and end time work together for Youtube video. - Given we a video in "Youtube" mode with start time set to 00:00:10 and end_time set to 00:00:15 - And I see video slider at "0:10" position - And I click video button "play" - Then I wait until video stop playing - Then I see video slider at "0:15" position - - """ - data = {'start_time': '00:00:10', 'end_time': '00:00:15'} - self.metadata = self.metadata_for_mode('youtube', additional_data=data) - - # go to video - self.navigate_to_video() - - self.assertEqual(self.video.position, '0:10') - - self.video.click_player_button('play') - - # wait until video stop playing - self.video.wait_for_state('pause') - - self.assertIn(self.video.position, ('0:15', '0:16'))