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'))