Merge pull request #24432 from edly-io/ziafazal/bd-18-bockchoy-tests-removal

[BD-18] Remove bockchoy tests
This commit is contained in:
Feanil Patel
2020-07-16 09:39:26 -04:00
committed by GitHub
115 changed files with 67 additions and 35473 deletions

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

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

View File

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

View File

@@ -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()

View File

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

View File

@@ -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

View File

@@ -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 <span> 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'</span>(.+)<span')
def _clean_seq_titles(self, element):
"""
Clean HTML of sequence titles, stripping out span tags and returning the first line.
"""
return self.REMOVE_SPAN_TAG_RE.search(element.get_attribute('innerHTML')).groups()[0].strip()
# TODO: TNL-6546: Remove. This is no longer needed.
@property
def active_subsection_url(self):
"""
return the url of the active subsection in the left nav
"""
return self.q(css='.chapter-content-container .menu-item.active a').attrs('href')[0]
# TODO: TNL-6546: Remove all references to self.course_outline_page
# TODO: TNL-6546: Remove the following function
def visit_course_outline_page(self):
# use course_outline_page version of the nav
self.course_outline_page = True
# reload the same page with the course_outline_page flag
self.browser.get(self.browser.current_url + "&course_experience.course_outline_page=1")
self.wait_for_page()
class RenderXBlockPage(PageObject, CompletionOnViewMixin):
"""
render_xblock page.
"""
xblock_component_selector = '.xblock'
def __init__(self, browser, block_id):
super(RenderXBlockPage, self).__init__(browser)
self.block_id = block_id
@property
def url(self):
"""
Construct a URL to the page within the course.
"""
return BASE_URL + "/xblock/" + self.block_id
def is_browser_on_page(self):
return self.q(css='.course-content').present

View File

@@ -1,49 +0,0 @@
"""
Courseware search
"""
from common.test.acceptance.pages.lms.course_page import CoursePage
class CoursewareSearchPage(CoursePage):
"""
Coursware page featuring a search form
"""
url_path = "courseware/"
search_bar_selector = '#courseware-search-bar'
search_results_selector = '.courseware-results'
@property
def search_results(self):
""" search results list showing """
return self.q(css=self.search_results_selector)
def is_browser_on_page(self):
""" did we find the search bar in the UI """
return self.q(css=self.search_bar_selector).present
def enter_search_term(self, text):
""" enter the search term into the box """
self.q(css=self.search_bar_selector + ' input[type="text"]').fill(text)
def search(self):
""" execute the search """
self.q(css=self.search_bar_selector + ' [type="submit"]').click()
self.wait_for_ajax()
self.wait_for_element_visibility(self.search_results_selector, 'Search results are visible')
def search_for_term(self, text):
"""
Fill input and do search
"""
self.enter_search_term(text)
self.search()
def clear_search(self):
"""
Clear search bar after search.
"""
self.q(css=self.search_bar_selector + ' .cancel-button').click()
self.wait_for_element_visibility('#course-content', 'Search bar is cleared')

View File

@@ -1,78 +0,0 @@
"""Mode creation page (used to add modes to courses during testing)."""
import re
import six.moves.urllib.parse
from bok_choy.page_object import PageObject
from common.test.acceptance.pages.lms import BASE_URL
class ModeCreationPage(PageObject):
"""The mode creation page.
When allowed by the Django settings file, visiting this page allows modes to be
created for an existing course.
"""
def __init__(self, browser, course_id, mode_slug=None, mode_display_name=None, min_price=None,
suggested_prices=None, currency=None, sku=None):
"""The mode creation page is an endpoint for HTTP GET requests.
By default, it will create an 'honor' mode for the given course with display name
'Honor Code', a minimum price of 0, no suggested prices, and using USD as the currency.
Arguments:
browser (Browser): The browser instance.
course_id (unicode): The ID of the course for which modes are to be created.
Keyword Arguments:
mode_slug (str): The mode to add, either 'honor', 'verified', or 'professional'
mode_display_name (str): Describes the new course mode
min_price (int): The minimum price a user must pay to enroll in the new course mode
suggested_prices (str): Comma-separated prices to suggest to the user.
currency (str): The currency in which to list prices.
sku (str): The product SKU value.
"""
super(ModeCreationPage, self).__init__(browser)
self._course_id = course_id
self._parameters = {}
if mode_slug is not None:
self._parameters['mode_slug'] = mode_slug
if mode_display_name is not None:
self._parameters['mode_display_name'] = mode_display_name
if min_price is not None:
self._parameters['min_price'] = min_price
if suggested_prices is not None:
self._parameters['suggested_prices'] = suggested_prices
if currency is not None:
self._parameters['currency'] = currency
if sku is not None:
self._parameters['sku'] = sku
@property
def url(self):
"""Construct the mode creation URL."""
url = '{base}/course_modes/create_mode/{course_id}/'.format(
base=BASE_URL,
course_id=self._course_id
)
query_string = six.moves.urllib.parse.urlencode(self._parameters)
if query_string:
url += '?' + query_string
return url
def is_browser_on_page(self):
message = self.q(css='BODY').text[0]
match = re.search(r'Mode ([^$]+) created for ([^$]+).$', message)
return True if match else False

View File

@@ -5,8 +5,6 @@ Student dashboard page.
from bok_choy.page_object import PageObject
from opaque_keys.edx.keys import CourseKey
from six.moves import range
from common.test.acceptance.pages.lms import BASE_URL
@@ -21,223 +19,8 @@ class DashboardPage(PageObject):
def is_browser_on_page(self):
return self.q(css='.my-courses').present
@property
def current_courses_text(self):
"""
This is the title label for the section of the student dashboard that
shows all the courses that the student is enrolled in.
The string displayed is defined in lms/templates/dashboard.html.
"""
text_items = self.q(css='#my-courses').text
if len(text_items) > 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 <article> 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

View File

@@ -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()

View File

@@ -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]

View File

@@ -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

View File

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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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]

View File

@@ -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()

View File

@@ -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(' <span', 1)[0] for hint_html in hints_html]
@property
def hint_text(self):
"""
Return the "hint" text of the problem from its div.
"""
return self.q(css="div.problem .notification-hint .notification-message").text[0]
def verify_mathjax_rendered_in_problem(self):
"""
Check that MathJax have been rendered in problem hint
"""
def mathjax_present():
""" Returns True if MathJax css is present in the problem body """
mathjax_container = self.q(css="div.problem p .MathJax")
return mathjax_container.visible and mathjax_container.present
self.wait_for(
mathjax_present,
description="MathJax rendered in problem body"
)
def verify_mathjax_rendered_in_preview(self):
"""
Check that MathJax has been rendered in formula problem preview
"""
def mathjax_present():
""" Returns True if MathJax css is present inside the preview """
mathjax_container = self.q(css="div.problem div .MathJax")
return mathjax_container.visible and mathjax_container.present
self.wait_for(
mathjax_present,
description="MathJax rendered in problem preview"
)
def verify_mathjax_rendered_in_hint(self):
"""
Check that MathJax have been rendered in problem hint
"""
def mathjax_present():
""" Returns True if MathJax css is present in the problem body """
mathjax_container = self.q(css="div.problem div.problem-hint .MathJax")
return mathjax_container.visible and mathjax_container.present
self.wait_for(
mathjax_present,
description="MathJax rendered in hint"
)
def fill_answer(self, text, input_num=None):
"""
Fill in the answer to the problem.
args:
text: String to fill the input with.
kwargs:
input_num: If provided, fills only the input_numth field. Else, all
input fields will be filled.
"""
fields = self.q(css='div.problem div.capa_inputtype.textline input')
fields = fields.nth(input_num) if input_num is not None else fields
fields.fill(text)
def fill_answer_numerical(self, text):
"""
Fill in the answer to a numerical problem.
"""
self.q(css='div.problem div.inputtype input').fill(text)
self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear')
self.wait_for_ajax()
@property
def get_numerical_input_value(self):
"""
Get the numerical problem input contents
"""
return self.q(css='div.problem div.inputtype input').text[0]
def click_submit(self):
"""
Click the Submit button.
"""
click_css(self, '.problem .submit', require_notification=False)
def click_save(self):
"""
Click the Save button.
"""
click_css(self, '.problem .save', require_notification=False)
def click_reset(self):
"""
Click the Reset button.
"""
click_css(self, '.problem .reset', require_notification=False)
self.wait_for_ajax()
def click_show(self):
"""
Click the Show Answer button.
"""
css = '.problem .show'
# First make sure that the button visible and can be clicked on.
self.scroll_to_element(css)
self.q(css=css).click()
self.wait_for_ajax()
def is_hint_notification_visible(self):
"""
Is the Hint Notification visible?
"""
return self.q(css='.notification.notification-hint').visible
def is_feedback_message_notification_visible(self):
"""
Is the Feedback Messaged notification visible
"""
return self.q(css='.wrapper-problem-response .message').visible
def is_save_notification_visible(self):
"""
Is the Save Notification Visible?
"""
return self.q(css='.notification.warning.notification-save').visible
def is_success_notification_visible(self):
"""
Is the Submit Notification Visible?
"""
return self.q(css='.notification.success.notification-submit').visible
def wait_for_feedback_message_visibility(self):
"""
Wait for the Feedback Message notification to be visible.
"""
self.wait_for_element_visibility('.wrapper-problem-response .message',
'Waiting for the Feedback message to be visible')
def wait_for_save_notification(self):
"""
Wait for the Save Notification to be present
"""
self.wait_for_element_visibility('.notification.warning.notification-save',
'Waiting for Save notification to be visible')
self.wait_for(lambda: self.q(css='.notification.warning.notification-save').focused,
'Waiting for the focus to be on the save notification')
def wait_for_gentle_alert_notification(self):
"""
Wait for the Gentle Alert Notification to be present
"""
self.wait_for_element_visibility('.notification.warning.notification-gentle-alert',
'Waiting for Gentle Alert notification to be visible')
self.wait_for(lambda: self.q(css='.notification.warning.notification-gentle-alert').focused,
'Waiting for the focus to be on the gentle alert notification')
def wait_for_show_answer_notification(self):
"""
Wait for the show answer Notification to be present
"""
self.wait_for_element_visibility('.notification.general.notification-show-answer',
'Waiting for Show Answer notification to be visible')
self.wait_for(lambda: self.q(css='.notification.general.notification-show-answer').focused,
'Waiting for the focus to be on the show answer notification')
def is_gentle_alert_notification_visible(self):
"""
Is the Gentle Alert Notification visible?
"""
return self.q(css='.notification.warning.notification-gentle-alert').visible
def is_reset_button_present(self):
""" Check for the presence of the reset button. """
return self.q(css='.problem .reset').present
def is_save_button_enabled(self):
""" Is the Save button enabled """
return self.q(css='.action .save').attrs('disabled') == [None]
def is_focus_on_problem_meta(self):
"""
Check for focus problem meta.
"""
return self.q(css='.problem-header').focused
def wait_for_focus_on_problem_meta(self):
"""
Waits for focus on Problem Meta section
"""
self.wait_for(
promise_check_func=self.is_focus_on_problem_meta,
description='Waiting for focus on Problem Meta section'
)
def is_submit_disabled(self):
"""
Checks if the submit button is disabled
"""
disabled_attr = self.q(css='.problem .submit').attrs('disabled')[0]
return disabled_attr == 'true'
def wait_for_submit_disabled(self):
"""
Waits until the Submit button becomes disabled.
"""
self.wait_for(self.is_submit_disabled, 'Waiting for submit to be enabled')
def wait_for_focus_on_submit_notification(self):
"""
Check for focus submit notification.
"""
def focus_check():
"""
Checks whether or not the focus is on the notification-submit
"""
return self.q(css='.notification-submit').focused
self.wait_for(promise_check_func=focus_check, description='Waiting for the notification-submit to gain focus')
def wait_for_status_icon(self):
"""
wait for status icon
"""
self.wait_for_element_visibility('div.problem div.inputtype div .status', 'wait for status icon')
def wait_for_expected_status(self, status_selector, message):
"""
Waits for the expected status indicator.
Args:
status_selector(str): status selector string.
message(str): description of promise, to be logged.
"""
msg = u"Wait for status to be {}".format(message)
self.wait_for_element_visibility(status_selector, msg)
def is_expected_status_visible(self, status_selector):
"""
check for the expected status indicator to be visible.
Args:
status_selector(str): status selector string.
"""
return self.q(css=status_selector).visible
def wait_success_notification(self):
"""
Check for visibility of the success notification and icon.
"""
msg = "Wait for success notification to be visible"
self.wait_for_element_visibility('.notification.success.notification-submit', msg)
self.wait_for_element_visibility('.fa-check', "Waiting for success icon")
self.wait_for_focus_on_submit_notification()
def wait_incorrect_notification(self):
"""
Check for visibility of the incorrect notification and icon.
"""
msg = "Wait for error notification to be visible"
self.wait_for_element_visibility('.notification.error.notification-submit', msg)
self.wait_for_element_visibility('.fa-close', "Waiting for incorrect notification icon")
self.wait_for_focus_on_submit_notification()
def wait_partial_notification(self):
"""
Check for visibility of the partially visible notification and icon.
"""
msg = "Wait for partial correct notification to be visible"
self.wait_for_element_visibility('.notification.success.notification-submit', msg)
self.wait_for_element_visibility('.fa-asterisk', "Waiting for asterisk notification icon")
self.wait_for_focus_on_submit_notification()
def wait_submitted_notification(self):
"""
Check for visibility of the "answer received" general notification and icon.
"""
msg = "Wait for submitted notification to be visible"
self.wait_for_element_visibility('.notification.general.notification-submit', msg)
self.wait_for_focus_on_submit_notification()
def click_hint(self, hint_index=0):
"""
Click the Hint button.
Arguments:
hint_index (int): Index of a displayed hint
"""
click_css(self, '.problem .hint-button', require_notification=False)
self.wait_for_focus_on_hint_notification(hint_index)
def wait_for_focus_on_hint_notification(self, hint_index=0):
"""
Wait for focus to be on the hint notification.
Arguments:
hint_index (int): Index of a displayed hint
"""
css = u'.notification-hint .notification-message > 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 <clarification> element:
Problem <clarification>clarification text hidden by an icon in rendering</clarification> 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()

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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'.")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
"""

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

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

View File

@@ -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]

View File

@@ -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',
]

View File

@@ -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()

View File

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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]

View File

@@ -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.

View File

@@ -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()

View File

@@ -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 "{}(<browser>, {!r})".format(self.__class__.__name__, self.context_selector)

View File

@@ -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()

View File

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

View File

@@ -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.

View File

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

View File

@@ -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

View File

@@ -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'<p><a href="{}">{}</a></p>'.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'<p><img src="{}" alt="{}" title=""></p>'.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'<p>Some content<img src="{}" alt="{}" title=""></p>'.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(), "<p>Some plain text</p>")
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(), (
"<p>Some markdown</p>\n"
"\n"
"<ul>\n"
"<li>line 1</li>\n"
"<li>line 2</li>\n"
"</ul>"
))
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)

View File

@@ -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.")

View File

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

View File

@@ -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'<h1>another name<h1>', u'<script>'],
'Full Name cannot contain the following characters: < >',
False
)
def test_email_field(self):
"""
Test behaviour of "Email" field.
"""
email = u"test@example.com"
username, user_id = self.log_in_as_unique_user(email=email)
self.visit_account_settings_page()
self._test_text_field(
u'email',
u'Email Address (Sign In)',
email,
u'test@example.com' + XSS_INJECTION,
[u'me@here.com', u'you@there.com'],
success_message='Click the link in the message to update your email address.',
assert_after_reload=False
)
actual_events = self.wait_for_events(
event_filter=self.settings_change_initiated_event_filter, number_of_matches=2)
self.assert_events_match(
[
self.expected_settings_change_initiated_event(
'email', email, 'me@here.com', username=username, user_id=user_id),
# NOTE the first email change was never confirmed, so old has not changed.
self.expected_settings_change_initiated_event(
'email', email, 'you@there.com', username=username, user_id=user_id),
],
actual_events
)
# Email is not saved until user confirms, so no events should have been
# emitted.
self.assert_no_setting_changed_event()
def test_password_field(self):
"""
Test behaviour of "Password" field.
"""
self._test_link_field(
u'password',
u'Password',
u'Reset Your Password',
u'button',
success_message='Click the link in the message to reset your password.',
)
event_filter = self.expected_settings_change_initiated_event('password', None, None)
self.wait_for_events(event_filter=event_filter, number_of_matches=1)
# Like email, since the user has not confirmed their password change,
# the field has not yet changed, so no events will have been emitted.
self.assert_no_setting_changed_event()
@skip(
'On bokchoy test servers, language changes take a few reloads to fully realize '
'which means we can no longer reliably match the strings in the html in other tests.'
)
def test_language_field(self):
"""
Test behaviour of "Language" field.
"""
self._test_dropdown_field(
u'pref-lang',
u'Language',
u'English',
[u'Dummy Language (Esperanto)', u'English'],
reloads_on_save=True,
)
def test_country_field(self):
"""
Test behaviour of "Country or Region" field.
"""
self._test_dropdown_field(
u'country',
u'Country or Region of Residence',
u'',
[u'Pakistan', u'Palau'],
)
def test_time_zone_field(self):
"""
Test behaviour of "Time Zone" field
"""
kiev_abbr, kiev_offset = self._get_time_zone_info('Europe/Kiev')
pacific_abbr, pacific_offset = self._get_time_zone_info('US/Pacific')
self._test_dropdown_field(
u'time_zone',
u'Time Zone',
u'Default (Local Time Zone)',
[
u'Europe/Kiev ({abbr}, UTC{offset})'.format(abbr=kiev_abbr, offset=kiev_offset),
u'US/Pacific ({abbr}, UTC{offset})'.format(abbr=pacific_abbr, offset=pacific_offset),
],
)
def _get_time_zone_info(self, time_zone_str):
"""
Helper that returns current time zone abbreviation and UTC offset
and accounts for daylight savings time
"""
time_zone = datetime.now(utc).astimezone(timezone(time_zone_str))
abbr = time_zone.strftime('%Z')
offset = time_zone.strftime('%z')
return abbr, offset
def test_social_links_field(self):
"""
Test behaviour of one of the social media links field.
"""
first_social_media_link = self.account_settings_page.get_social_first_element()
valid_value = 'https://www.twitter.com/edX'
if 'face' in first_social_media_link.lower():
valid_value = 'https://www.facebook.com/edX'
elif 'linked' in first_social_media_link.lower():
valid_value = 'https://www.linkedin.com/in/edX'
self._test_text_field(
'social_links',
first_social_media_link,
self.social_link,
'www.google.com/invalidlink)',
[valid_value, self.social_link],
)
def test_linked_accounts(self):
"""
Test that fields for third party auth providers exist.
Currently there is no way to test the whole authentication process
because that would require accounts with the providers.
"""
providers = (
['auth-oa2-facebook', 'Facebook', 'Link Your Account'],
['auth-oa2-google-oauth2', 'Google', 'Link Your Account'],
)
# switch to "Linked Accounts" tab
self.account_settings_page.switch_account_settings_tabs('accounts-tab')
for field_id, title, link_title in providers:
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)
def test_order_history(self):
"""
Test that we can see orders on Order History tab.
"""
# switch to "Order History" tab
self.account_settings_page.switch_account_settings_tabs('orders-tab')
# verify that we are on correct tab
self.assertTrue(self.account_settings_page.is_order_history_tab_visible)
expected_order_data_first_row = {
'number': 'Order Number:\nEdx-123',
'date': 'Date Placed:\nApr 21, 2016',
'price': 'Cost:\n$100.00',
}
expected_order_data_second_row = {
'number': 'Product Name:\nTest Course',
'date': 'Date Placed:\nApr 21, 2016',
'price': 'Cost:\n$100.00',
}
for field_name, value in six.iteritems(expected_order_data_first_row):
self.assertEqual(
self.account_settings_page.get_value_of_order_history_row_item('order-Edx-123', field_name)[0], value
)
for field_name, value in six.iteritems(expected_order_data_second_row):
self.assertEqual(
self.account_settings_page.get_value_of_order_history_row_item('order-Edx-123', field_name)[1], value
)
self.assertTrue(self.account_settings_page.order_button_is_visible('order-Edx-123'))
class AccountSettingsDeleteAccountTest(AccountSettingsTestMixin, AcceptanceTest):
"""
Tests for the account deletion workflow.
"""
def setUp(self):
"""
Initialize account and pages.
"""
super(AccountSettingsDeleteAccountTest, self).setUp()
self.full_name = FULL_NAME
self.social_link = ''
self.password = 'password'
self.username, self.user_id = self.log_in_as_unique_user(full_name=self.full_name, password=self.password)
self.visit_account_settings_page(gdpr=True)
def test_button_visible(self):
self.assertTrue(
self.account_settings_page.is_delete_button_visible
)
def test_delete_modal(self):
self.account_settings_page.click_delete_button()
self.assertTrue(
self.account_settings_page.is_delete_modal_visible
)
self.assertFalse(
self.account_settings_page.delete_confirm_button_enabled()
)
self.account_settings_page.fill_in_password_field(self.password)
self.assertTrue(
self.account_settings_page.delete_confirm_button_enabled()
)
class AccountSettingsA11yTest(AccountSettingsTestMixin, AcceptanceTest):
"""

View File

@@ -1,92 +0,0 @@
"""
Acceptance tests for the certificate web view feature.
"""
from common.test.acceptance.fixtures.certificates import CertificateConfigFixture
from common.test.acceptance.fixtures.course import CourseFixture
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.certificate_page import CertificatePage
from common.test.acceptance.tests.helpers import EventsTestMixin, UniqueCourseTest
class CertificateWebViewTest(EventsTestMixin, UniqueCourseTest):
"""
Tests for verifying certificate web view features
"""
shard = 5
def setUp(self):
super(CertificateWebViewTest, self).setUp()
# set same course number as we have in fixture json
self.course_info['number'] = "335535897951379478207964576572017930000"
test_certificate_config = {
'id': 1,
'name': 'Certificate name',
'description': 'Certificate description',
'course_title': 'Course title override',
'signatories': [],
'version': 1,
'is_active': True,
}
course_settings = {'certificates': test_certificate_config}
self.course_fixture = CourseFixture(
self.course_info["org"],
self.course_info["number"],
self.course_info["run"],
self.course_info["display_name"],
settings=course_settings
)
self.course_fixture.add_advanced_settings({
"cert_html_view_enabled": {"value": "true"},
"certificates_display_behavior": {"value": "early_with_info"},
})
self.course_fixture.install()
self.user_id = "99" # we have created a user with this id in fixture
self.cert_fixture = CertificateConfigFixture(self.course_id, test_certificate_config)
# Load certificate web view page for use by the tests
self.certificate_page = CertificatePage(self.browser, self.user_id, self.course_id)
def log_in_as_unique_user(self):
"""
Log in as a valid lms user.
"""
AutoAuthPage(
self.browser,
username="testcert",
email="cert@example.com",
password="testuser",
course_id=self.course_id
).visit()
def test_page_has_accomplishments_banner(self):
"""
Scenario: User accomplishment banner should be present if logged in user is the one who is awarded
the certificate
Given there is a course with certificate configuration
And I have passed the course and certificate is generated
When I view the certificate web view page
Then I should see the accomplishment banner. banner should have linked-in and facebook share buttons
And When I click on `Add to Profile` button `edx.certificate.shared` event should be emitted
"""
self.cert_fixture.install()
self.log_in_as_unique_user()
self.certificate_page.visit()
self.assertTrue(self.certificate_page.accomplishment_banner.visible)
self.assertTrue(self.certificate_page.add_to_linkedin_profile_button.visible)
self.assertTrue(self.certificate_page.add_to_facebook_profile_button.visible)
self.certificate_page.add_to_linkedin_profile_button.click()
actual_events = self.wait_for_events(
event_filter={'event_type': 'edx.certificate.shared'},
number_of_matches=1
)
expected_events = [
{
'event': {
'user_id': self.user_id,
'course_id': self.course_id
}
}
]
self.assert_events_match(expected_events, actual_events)

View File

@@ -4,14 +4,10 @@ End-to-end tests for Student's Profile Page.
"""
from contextlib import contextmanager
from datetime import datetime
import six
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.account_settings import AccountSettingsPage
from common.test.acceptance.pages.lms.learner_profile import LearnerProfilePage
from common.test.acceptance.tests.helpers import AcceptanceTest, EventsTestMixin
@@ -86,81 +82,6 @@ class LearnerProfileTestMixin(EventsTestMixin):
return profile_page
def set_birth_year(self, birth_year):
"""
Set birth year for the current user to the specified value.
"""
account_settings_page = AccountSettingsPage(self.browser)
account_settings_page.visit()
account_settings_page.wait_for_page()
self.assertEqual(
account_settings_page.value_for_dropdown_field('year_of_birth', str(birth_year), focus_out=True),
str(birth_year)
)
def verify_profile_page_is_public(self, profile_page, is_editable=True):
"""
Verify that the profile page is currently public.
"""
self.assertEqual(profile_page.visible_fields, self.PUBLIC_PROFILE_FIELDS)
if is_editable:
self.assertTrue(profile_page.privacy_field_visible)
self.assertEqual(profile_page.editable_fields, self.PUBLIC_PROFILE_EDITABLE_FIELDS)
else:
self.assertEqual(profile_page.editable_fields, [])
def verify_profile_page_is_private(self, profile_page, is_editable=True):
"""
Verify that the profile page is currently private.
"""
if is_editable:
self.assertTrue(profile_page.privacy_field_visible)
self.assertEqual(profile_page.visible_fields, self.PRIVATE_PROFILE_FIELDS)
def verify_profile_page_view_event(self, requesting_username, profile_user_id, visibility=None):
"""
Verifies that the correct view event was captured for the profile page.
"""
actual_events = self.wait_for_events(
start_time=self.start_time,
event_filter={'event_type': 'edx.user.settings.viewed', 'username': requesting_username},
number_of_matches=1)
self.assert_events_match(
[
{
'username': requesting_username,
'event': {
'user_id': int(profile_user_id),
'page': 'profile',
'visibility': six.text_type(visibility)
}
}
],
actual_events
)
@contextmanager
def verify_pref_change_event_during(self, username, user_id, setting, **kwargs):
"""Assert that a single setting changed event is emitted for the user_api_userpreference table."""
expected_event = {
'username': username,
'event': {
'setting': setting,
'user_id': int(user_id),
'table': 'user_api_userpreference',
'truncated': []
}
}
expected_event['event'].update(kwargs)
event_filter = {
'event_type': self.USER_SETTINGS_CHANGED_EVENT_NAME,
'username': username,
}
with self.assert_events_match_during(event_filter=event_filter, expected_events=[expected_event]):
yield
def initialize_different_user(self, privacy=None, birth_year=None):
"""
Initialize the profile page for a different test user

View File

@@ -1,284 +0,0 @@
# -*- coding: utf-8 -*-
"""
End-to-end tests for LibraryContent block in LMS
"""
import textwrap
import ddt
import six
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
from common.test.acceptance.fixtures.library import LibraryFixture
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.library import LibraryContentXBlockWrapper
from common.test.acceptance.pages.studio.library import StudioLibraryContainerXBlockWrapper, StudioLibraryContentEditor
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.tests.helpers import TestWithSearchIndexMixin, UniqueCourseTest
SECTION_NAME = 'Test Section'
SUBSECTION_NAME = 'Test Subsection'
UNIT_NAME = 'Test Unit'
class LibraryContentTestBase(UniqueCourseTest):
""" Base class for library content block tests """
USERNAME = "STUDENT_TESTER"
EMAIL = "student101@example.com"
STAFF_USERNAME = "STAFF_TESTER"
STAFF_EMAIL = "staff101@example.com"
shard = 10
def populate_library_fixture(self, library_fixture):
"""
To be overwritten by subclassed tests. Used to install a library to
run tests on.
"""
def setUp(self):
"""
Set up library, course and library content XBlock
"""
super(LibraryContentTestBase, 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']
)
self.library_fixture = LibraryFixture('test_org', self.unique_id, u'Test Library {}'.format(self.unique_id))
self.populate_library_fixture(self.library_fixture)
self.library_fixture.install()
self.library_info = self.library_fixture.library_info
self.library_key = self.library_fixture.library_key
# Install a course with library content xblock
self.course_fixture = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
library_content_metadata = {
'source_library_id': six.text_type(self.library_key),
'mode': 'random',
'max_count': 1,
}
self.lib_block = XBlockFixtureDesc('library_content', "Library Content", metadata=library_content_metadata)
self.course_fixture.add_children(
XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children(
XBlockFixtureDesc('vertical', UNIT_NAME).add_children(
self.lib_block
)
)
)
)
self.course_fixture.install()
def _change_library_content_settings(self, count=1, capa_type=None):
"""
Performs library block refresh in Studio, configuring it to show {count} children
"""
unit_page = self._go_to_unit_page(True)
library_container_block = StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(unit_page.xblocks[1])
library_container_block.edit()
editor = StudioLibraryContentEditor(self.browser, library_container_block.locator)
editor.count = count
if capa_type is not None:
editor.capa_type = capa_type
editor.save()
self._go_to_unit_page(change_login=False)
unit_page.wait_for_page()
unit_page.publish()
self.assertIn("Published and Live", unit_page.publish_title)
@property
def library_xblocks_texts(self):
"""
Gets texts of all xblocks in library
"""
return frozenset(child.data for child in self.library_fixture.children)
def _go_to_unit_page(self, change_login=True):
"""
Open unit page in Studio
"""
if change_login:
LogoutPage(self.browser).visit()
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.studio_course_outline.visit()
subsection = self.studio_course_outline.section(SECTION_NAME).subsection(SUBSECTION_NAME)
return subsection.expand_subsection().unit(UNIT_NAME).go_to()
def _goto_library_block_page(self, block_id=None):
"""
Open library page in LMS
"""
self.courseware_page.visit()
paragraphs = self.courseware_page.q(css='.course-content p').results
if not paragraphs:
course_home_page = CourseHomePage(self.browser, self.course_id)
course_home_page.visit()
course_home_page.outline.go_to_section_by_index(0, 0)
block_id = block_id if block_id is not None else self.lib_block.locator
#pylint: disable=attribute-defined-outside-init
self.library_content_page = LibraryContentXBlockWrapper(self.browser, block_id)
self.library_content_page.wait_for_page()
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()
@ddt.ddt
class LibraryContentTest(LibraryContentTestBase):
"""
Test courseware.
"""
shard = 10
def populate_library_fixture(self, library_fixture):
"""
Populates library fixture with XBlock Fixtures
"""
library_fixture.add_children(
XBlockFixtureDesc("html", "Html1", data='html1'),
XBlockFixtureDesc("html", "Html2", data='html2'),
XBlockFixtureDesc("html", "Html3", data='html3'),
XBlockFixtureDesc("html", "Html4", data='html4'),
)
@ddt.data(2, 3, 4)
def test_shows_random_xblocks_from_configured(self, count):
"""
Scenario: Ensures that library content shows {count} random xblocks from library in LMS
Given I have a library, a course and a LibraryContent block in that course
When I go to studio unit page for library content xblock as staff
And I set library content xblock to display {count} random children
And I refresh library content xblock and pulbish unit
When I go to LMS courseware page for library content xblock as student
Then I can see {count} random xblocks from the library
"""
self._change_library_content_settings(count=count)
self._auto_auth(self.USERNAME, self.EMAIL, False)
self._goto_library_block_page()
children_contents = self.library_content_page.children_contents
self.assertEqual(len(children_contents), count)
self.assertLessEqual(children_contents, self.library_xblocks_texts)
def test_shows_all_if_max_set_to_greater_value(self):
"""
Scenario: Ensures that library content shows {count} random xblocks from library in LMS
Given I have a library, a course and a LibraryContent block in that course
When I go to studio unit page for library content xblock as staff
And I set library content xblock to display more children than library have
And I refresh library content xblock and pulbish unit
When I go to LMS courseware page for library content xblock as student
Then I can see all xblocks from the library
"""
self._change_library_content_settings(count=10)
self._auto_auth(self.USERNAME, self.EMAIL, False)
self._goto_library_block_page()
children_contents = self.library_content_page.children_contents
self.assertEqual(len(children_contents), 4)
self.assertEqual(children_contents, self.library_xblocks_texts)
@ddt.ddt
class StudioLibraryContainerCapaFilterTest(LibraryContentTestBase, TestWithSearchIndexMixin):
"""
Test Library Content block in LMS
"""
shard = 10
def setUp(self):
""" SetUp method """
self._create_search_index()
super(StudioLibraryContainerCapaFilterTest, self).setUp()
def tearDown(self):
self._cleanup_index_file()
super(StudioLibraryContainerCapaFilterTest, self).tearDown()
def _get_problem_choice_group_text(self, name, items):
""" Generates Choice Group CAPA problem XML """
items_text = "\n".join([
u"<choice correct='{correct}'>{item}</choice>".format(correct=correct, item=item)
for item, correct in items
])
return textwrap.dedent(u"""
<problem>
<p>{name}</p>
<multiplechoiceresponse>
<choicegroup label="{name}" type="MultipleChoice">{items}</choicegroup>
</multiplechoiceresponse>
</problem>""").format(name=name, items=items_text)
def _get_problem_select_text(self, name, items, correct):
""" Generates Select Option CAPA problem XML """
items_text = ",".join(["'{0}'".format(item) for item in items])
return textwrap.dedent(u"""
<problem>
<p>{name}</p>
<optionresponse>
<optioninput label="{name}" options="({options})" correct="{correct}"></optioninput>
</optionresponse>
</problem>""").format(name=name, options=items_text, correct=correct)
def populate_library_fixture(self, library_fixture):
"""
Populates library fixture with XBlock Fixtures
"""
items = (
XBlockFixtureDesc(
"problem", "Problem Choice Group 1",
data=self._get_problem_choice_group_text("Problem Choice Group 1 Text", [("1", False), ('2', True)])
),
XBlockFixtureDesc(
"problem", "Problem Choice Group 2",
data=self._get_problem_choice_group_text("Problem Choice Group 2 Text", [("Q", True), ('W', False)])
),
XBlockFixtureDesc(
"problem", "Problem Select 1",
data=self._get_problem_select_text("Problem Select 1 Text", ["Option 1", "Option 2"], "Option 1")
),
XBlockFixtureDesc(
"problem", "Problem Select 2",
data=self._get_problem_select_text("Problem Select 2 Text", ["Option 3", "Option 4"], "Option 4")
),
)
library_fixture.add_children(*items)
@property
def _problem_headers(self):
""" Expected XBLock headers according to populate_library_fixture """
return frozenset(child.display_name for child in self.library_fixture.children)
def _set_library_content_settings(self, count=1, capa_type="Any Type"):
"""
Sets library content XBlock parameters, saves, publishes unit, goes to LMS unit page and
gets children XBlock headers to assert against them
"""
self._change_library_content_settings(count=count, capa_type=capa_type)
self._auto_auth(self.USERNAME, self.EMAIL, False)
self._goto_library_block_page()
return self.library_content_page.children_headers

View File

@@ -4,18 +4,8 @@ End-to-end tests for the LMS.
"""
import json
from datetime import datetime, timedelta
from textwrap import dedent
import pytz
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.lms import BASE_URL
from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage
from common.test.acceptance.pages.lms.course_about import CourseAboutPage
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.course_wiki import (
CourseWikiChildrenPage,
@@ -23,375 +13,13 @@ from common.test.acceptance.pages.lms.course_wiki import (
CourseWikiHistoryPage,
CourseWikiPage
)
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.discovery import CourseDiscoveryPage
from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage, ResetPasswordPage
from common.test.acceptance.pages.lms.problem import ProblemPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
from common.test.acceptance.tests.helpers import (
EventsTestMixin,
UniqueCourseTest,
remove_file,
)
from openedx.core.lib.tests import attr
@attr(shard=19)
class ForgotPasswordPageTest(UniqueCourseTest):
"""
Test that forgot password forms is rendered if url contains 'forgot-password-modal'
in hash.
"""
def setUp(self):
""" Initialize the page object """
super(ForgotPasswordPageTest, self).setUp()
self.user_info = self._create_user()
self.reset_password_page = ResetPasswordPage(self.browser)
def _create_user(self):
"""
Create a unique user
"""
auto_auth = AutoAuthPage(self.browser).visit()
user_info = auto_auth.user_info
LogoutPage(self.browser).visit()
return user_info
def test_reset_password_form_visibility(self):
# Navigate to the password reset page
self.reset_password_page.visit()
# Expect that reset password form is visible on the page
self.assertTrue(self.reset_password_page.is_form_visible())
def test_reset_password_confirmation_box_visibility(self):
# Navigate to the password reset page
self.reset_password_page.visit()
# Navigate to the password reset form and try to submit it
self.reset_password_page.fill_password_reset_form(self.user_info['email'])
self.reset_password_page.is_success_visible(".submission-success")
# Expect that we're shown a success message
self.assertIn("Check Your Email", self.reset_password_page.get_success_message())
@attr(shard=19)
class LoginFromCombinedPageTest(UniqueCourseTest):
"""Test that we can log in using the combined login/registration page.
Also test that we can request a password reset from the combined
login/registration page.
"""
def setUp(self):
"""Initialize the page objects and create a test course. """
super(LoginFromCombinedPageTest, self).setUp()
self.login_page = CombinedLoginAndRegisterPage(
self.browser,
start_page="login",
course_id=self.course_id
)
self.dashboard_page = DashboardPage(self.browser)
# Create a course to enroll in
CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
).install()
def test_login_success(self):
# Create a user account
email, password = self._create_unique_user()
# Navigate to the login page and try to log in
self.login_page.visit().login(email=email, password=password)
# Expect that we reach the dashboard and we're auto-enrolled in the course
course_names = self.dashboard_page.wait_for_page().available_courses
self.assertIn(self.course_info["display_name"], course_names)
def test_login_failure(self):
# Navigate to the login page
self.login_page.visit()
# User account does not exist
self.login_page.login(email="nobody@nowhere.com", password="password")
# Verify that an error is displayed
self.assertIn("Email or password is incorrect.", self.login_page.wait_for_errors())
def test_toggle_to_register_form(self):
self.login_page.visit().toggle_form()
self.assertEqual(self.login_page.current_form, "register")
def test_password_reset_success(self):
# Create a user account
email, password = self._create_unique_user() # pylint: disable=unused-variable
# Navigate to the password reset form and try to submit it
self.login_page.visit().password_reset(email=email)
# Expect that we're shown a success message
self.assertIn("Check Your Email", self.login_page.wait_for_success())
def test_password_reset_no_user(self):
# Navigate to the password reset form
self.login_page.visit()
# User account does not exist
self.login_page.password_reset(email="nobody@nowhere.com")
# Expect that we're shown a success message
self.assertIn("Check Your Email", self.login_page.wait_for_success())
def test_third_party_login(self):
"""
Test that we can login using third party credentials, and that the
third party account gets linked to the edX account.
"""
# Create a user account
email, password = self._create_unique_user()
# Navigate to the login page
self.login_page.visit()
# Baseline screen-shots are different for chrome and firefox.
#self.assertScreenshot('#login .login-providers', 'login-providers-{}'.format(self.browser.name), .25)
#The line above is commented out temporarily see SOL-1937
# Try to log in using "Dummy" provider
self.login_page.click_third_party_dummy_provider()
# The user will be redirected somewhere and then back to the login page:
msg_text = self.login_page.wait_for_auth_status_message()
self.assertIn("You have successfully signed into Dummy", msg_text)
self.assertIn(
u"To link your accounts, sign in now using your édX password",
msg_text
)
# Now login with username and password:
self.login_page.login(email=email, password=password)
# Expect that we reach the dashboard and we're auto-enrolled in the course
course_names = self.dashboard_page.wait_for_page().available_courses
self.assertIn(self.course_info["display_name"], course_names)
try:
# Now logout and check that we can log back in instantly (because the account is linked):
LogoutPage(self.browser).visit()
self.login_page.visit()
self.login_page.click_third_party_dummy_provider()
self.dashboard_page.wait_for_page()
finally:
self._unlink_dummy_account()
def test_hinted_login(self):
""" Test the login page when coming from course URL that specified which third party provider to use """
# Create a user account and link it to third party auth with the dummy provider:
AutoAuthPage(self.browser, course_id=self.course_id).visit()
self._link_dummy_account()
try:
LogoutPage(self.browser).visit()
# When not logged in, try to load a course URL that includes the provider hint ?tpa_hint=...
course_page = CoursewarePage(self.browser, self.course_id)
self.browser.get(course_page.url + '?tpa_hint=oa2-dummy')
# We should now be redirected to the login page
self.login_page.wait_for_page()
self.assertIn(
"Would you like to sign in using your Dummy credentials?",
self.login_page.hinted_login_prompt
)
# Baseline screen-shots are different for chrome and firefox.
#self.assertScreenshot('#hinted-login-form', 'hinted-login-{}'.format(self.browser.name), .25)
#The line above is commented out temporarily see SOL-1937
self.login_page.click_third_party_dummy_provider()
# We should now be redirected to the course page
course_page.wait_for_page()
finally:
self._unlink_dummy_account()
def _link_dummy_account(self):
""" Go to Account Settings page and link the user's account to the Dummy provider """
account_settings = AccountSettingsPage(self.browser).visit()
# switch to "Linked Accounts" tab
account_settings.switch_account_settings_tabs('accounts-tab')
field_id = "auth-oa2-dummy"
account_settings.wait_for_field(field_id)
self.assertEqual("Link Your Account", account_settings.link_title_for_link_field(field_id))
account_settings.click_on_link_in_link_field(field_id)
# make sure we are on "Linked Accounts" tab after the account settings
# page is reloaded
account_settings.switch_account_settings_tabs('accounts-tab')
account_settings.wait_for_link_title_for_link_field(field_id, "Unlink This Account")
def _unlink_dummy_account(self):
""" Verify that the 'Dummy' third party auth provider is linked, then unlink it """
# This must be done after linking the account, or we'll get cross-test side effects
account_settings = AccountSettingsPage(self.browser).visit()
# switch to "Linked Accounts" tab
account_settings.switch_account_settings_tabs('accounts-tab')
field_id = "auth-oa2-dummy"
account_settings.wait_for_field(field_id)
self.assertEqual("Unlink This Account", account_settings.link_title_for_link_field(field_id))
account_settings.click_on_link_in_link_field(field_id)
account_settings.wait_for_message(field_id, "Successfully unlinked")
def _create_unique_user(self):
"""
Create a new user with a unique name and email.
"""
username = u"test_{uuid}".format(uuid=self.unique_id[0:6])
email = u"{user}@example.com".format(user=username)
password = "password"
# Create the user (automatically logs us in)
AutoAuthPage(
self.browser,
username=username,
email=email,
password=password
).visit()
# Log out
LogoutPage(self.browser).visit()
return (email, password)
@attr(shard=19)
class RegisterFromCombinedPageTest(UniqueCourseTest):
"""Test that we can register a new user from the combined login/registration page. """
def setUp(self):
"""Initialize the page objects and create a test course. """
super(RegisterFromCombinedPageTest, self).setUp()
self.register_page = CombinedLoginAndRegisterPage(
self.browser,
start_page="register",
course_id=self.course_id
)
self.dashboard_page = DashboardPage(self.browser)
# Create a course to enroll in
CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
).install()
def test_register_success(self):
# Navigate to the registration page
self.register_page.visit()
# Fill in the form and submit it
username = u"test_{uuid}".format(uuid=self.unique_id[0:6])
email = u"{user}@example.com".format(user=username)
self.register_page.register(
email=email,
password="password",
username=username,
full_name="Test User",
country="US",
favorite_movie="Mad Max: Fury Road"
)
# Expect that we reach the dashboard and we're auto-enrolled in the course
course_names = self.dashboard_page.wait_for_page().available_courses
self.assertIn(self.course_info["display_name"], course_names)
def test_register_failure(self):
# Navigate to the registration page
self.register_page.visit()
# Enter a blank for the username field, which is required
# Don't agree to the terms of service / honor code.
# Don't specify a country code, which is required.
# Don't specify a favorite movie.
username = u"test_{uuid}".format(uuid=self.unique_id[0:6])
email = u"{user}@example.com".format(user=username)
self.register_page.register(
email=email,
password="password",
username="",
full_name="Test User"
)
# Verify that the expected errors are displayed.
errors = self.register_page.wait_for_errors()
self.assertIn(u'Please enter your Public Username.', errors)
self.assertIn(u'Select your country or region of residence.', errors)
self.assertIn(u'Please tell us your favorite movie.', errors)
def test_toggle_to_login_form(self):
self.register_page.visit().toggle_form()
self.assertEqual(self.register_page.current_form, "login")
def test_third_party_register(self):
"""
Test that we can register using third party credentials, and that the
third party account gets linked to the edX account.
"""
# Navigate to the register page
self.register_page.visit()
# Baseline screen-shots are different for chrome and firefox.
#self.assertScreenshot('#register .login-providers', 'register-providers-{}'.format(self.browser.name), .25)
# The line above is commented out temporarily see SOL-1937
# Try to authenticate using the "Dummy" provider
self.register_page.click_third_party_dummy_provider()
# The user will be redirected somewhere and then back to the register page:
msg_text = self.register_page.wait_for_auth_status_message()
self.assertEqual(self.register_page.current_form, "register")
self.assertIn("You've successfully signed into Dummy", msg_text)
self.assertIn("We just need a little more information", msg_text)
# Now the form should be pre-filled with the data from the Dummy provider:
self.assertEqual(self.register_page.email_value, "adama@fleet.colonies.gov")
self.assertEqual(self.register_page.full_name_value, "William Adama")
self.assertIn("Galactica1", self.register_page.username_value)
# Set country and submit the form:
self.register_page.register(country="US", favorite_movie="Battlestar Galactica")
# Expect that we reach the dashboard and we're auto-enrolled in the course
course_names = self.dashboard_page.wait_for_page().available_courses
self.assertIn(self.course_info["display_name"], course_names)
# Now logout and check that we can log back in instantly (because the account is linked):
LogoutPage(self.browser).visit()
login_page = CombinedLoginAndRegisterPage(self.browser, start_page="login")
login_page.visit()
login_page.click_third_party_dummy_provider()
self.dashboard_page.wait_for_page()
# Now unlink the account (To test the account settings view and also to prevent cross-test side effects)
account_settings = AccountSettingsPage(self.browser).visit()
# switch to "Linked Accounts" tab
account_settings.switch_account_settings_tabs('accounts-tab')
field_id = "auth-oa2-dummy"
account_settings.wait_for_field(field_id)
self.assertEqual("Unlink This Account", account_settings.link_title_for_link_field(field_id))
account_settings.click_on_link_in_link_field(field_id)
account_settings.wait_for_message(field_id, "Successfully unlinked")
@attr('a11y')
class CourseWikiA11yTest(UniqueCourseTest):
"""
@@ -482,293 +110,3 @@ class CourseWikiA11yTest(UniqueCourseTest):
})
children_page.wait_for_page()
children_page.a11y_audit.check_for_accessibility_errors()
@attr(shard=1)
class VisibleToStaffOnlyTest(UniqueCourseTest):
"""
Tests that content with visible_to_staff_only set to True cannot be viewed by students.
"""
def setUp(self):
super(VisibleToStaffOnlyTest, self).setUp()
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', 'Subsection With Locked Unit').add_children(
XBlockFixtureDesc('vertical', 'Locked Unit', metadata={'visible_to_staff_only': True}).add_children(
XBlockFixtureDesc('html', 'Html Child in locked unit', data="<html>Visible only to staff</html>"),
),
XBlockFixtureDesc('vertical', 'Unlocked Unit').add_children(
XBlockFixtureDesc('html', 'Html Child in unlocked unit', data="<html>Visible only to all</html>"),
)
),
XBlockFixtureDesc('sequential', 'Unlocked Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('html', 'Html Child in visible unit', data="<html>Visible to all</html>"),
)
),
XBlockFixtureDesc('sequential', 'Locked Subsection', metadata={'visible_to_staff_only': True}).add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc(
'html', 'Html Child in locked subsection', data="<html>Visible only to staff</html>"
)
)
)
)
).install()
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
def test_visible_to_student(self):
"""
Scenario: Content marked 'visible_to_staff_only' is not visible for students in the course
Given some of the course content has been marked 'visible_to_staff_only'
And I am logged on with an authorized student account
Then I can only see content without 'visible_to_staff_only' set to True
"""
AutoAuthPage(self.browser, username="STUDENT_TESTER", email="johndoe_student@example.com",
course_id=self.course_id, staff=False).visit()
self.course_home_page.visit()
self.assertEqual(2, len(self.course_home_page.outline.sections['Test Section']))
self.course_home_page.outline.go_to_section("Test Section", "Subsection With Locked Unit")
self.courseware_page.wait_for_page()
self.assertEqual([u'Unlocked Unit'], self.courseware_page.nav.sequence_items)
self.course_home_page.visit()
self.course_home_page.outline.go_to_section("Test Section", "Unlocked Subsection")
self.courseware_page.wait_for_page()
self.assertEqual([u'Test Unit'], self.courseware_page.nav.sequence_items)
@attr(shard=1)
class ProblemExecutionTest(UniqueCourseTest):
"""
Tests of problems.
"""
def setUp(self):
"""
Initialize pages and install a course fixture.
"""
super(ProblemExecutionTest, self).setUp()
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.tab_nav = TabNavPage(self.browser)
# Install a course with sections and problems.
course_fix = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
course_fix.add_asset(['python_lib.zip'])
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('problem', 'Python Problem', data=dedent(
"""\
<problem>
<script type="loncapa/python">
from number_helpers import seventeen, fortytwo
oneseven = seventeen()
def check_function(expect, ans):
if int(ans) == fortytwo(-22):
return True
else:
return False
</script>
<p>What is the sum of $oneseven and 3?</p>
<customresponse expect="20" cfn="check_function">
<textline/>
</customresponse>
</problem>
"""
))
)
)
).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'}
)

View File

@@ -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="<html>Contents</html>"),
)
)
)
)
).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)

View File

@@ -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='<html>GROUPACONTENT</html>'),
XBlockFixtureDesc('html', self.group_b_html, data='<html>GROUPBCONTENT</html>'),
XBlockFixtureDesc('html', self.group_a_and_b_html, data='<html>GROUPAANDBCONTENT</html>'),
XBlockFixtureDesc('html', self.visible_to_all_html, data='<html>VISIBLETOALLCONTENT</html>')
)
)
)
)
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

View File

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

View File

@@ -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

View File

@@ -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>problem 1 dummy body</problem>'),
XBlockFixtureDesc('html', 'html 1', data="<html>html 1 dummy body</html>"),
XBlockFixtureDesc('problem', 'Test Problem 2', data="<problem>problem 2 dummy body</problem>"),
XBlockFixtureDesc('html', 'html 2', data="<html>html 2 dummy body</html>"),
),
XBlockFixtureDesc('sequential', 'Test Subsection 1,2').add_children(
XBlockFixtureDesc('problem', 'Test Problem 3', data='<problem>problem 3 dummy body</problem>'),
),
XBlockFixtureDesc(
'sequential', 'Test HIDDEN Subsection', metadata={'visible_to_staff_only': True}
).add_children(
XBlockFixtureDesc('problem', 'Test HIDDEN Problem', data='<problem>hidden problem</problem>'),
),
),
XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 2,1').add_children(
XBlockFixtureDesc('problem', 'Test Problem 4', data='<problem>problem 4 dummy body</problem>'),
),
),
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>html 1 dummy body</html>")
self.problem_1_block = XBlockFixtureDesc(
'problem', 'Test Problem 1', data='<problem>problem 1 dummy body</problem>'
)
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>html 1 dummy body</html>"),
XBlockFixtureDesc(
'html', 'html 2',
data=("<html>html 2 dummy body</html>" * 100) + "<span id='html2-end'>End</span>",
),
XBlockFixtureDesc('problem', 'Test Problem 1', data='<problem>problem 1 dummy body</problem>'),
),
XBlockFixtureDesc('vertical', 'Test Unit 1,1,2').add_children(
XBlockFixtureDesc('html', 'html 1', data="<html>html 1 dummy body</html>"),
XBlockFixtureDesc('problem', 'Test Problem 1', data='<problem>problem 1 dummy body</problem>'),
),
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()

View File

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

View File

@@ -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' <span id="unenroll_course_name">' + course_name + u'</span>' + \
u' (<span id="unenroll_course_number">' + course_number + u'</span>)?'
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' <span id="unenroll_cert_name">' + cert_long_name + u'</span>' + \
u' track of <span id="unenroll_course_name">' + course_name + u'</span>' + \
u' (<span id="unenroll_course_number">' + course_number + u'</span>)?'
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)

View File

@@ -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("""
<problem>
<multiplechoiceresponse>
<label>What is height of eiffel tower without the antenna?.</label>
<choicegroup type="MultipleChoice">
<choice correct="false">324 meters<choicehint>Antenna is 24 meters high</choicehint></choice>
<choice correct="true">300 meters</choice>
<choice correct="false">224 meters</choice>
<choice correct="false">400 meters</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
return XBlockFixtureDesc('problem', 'HEIGHT OF EIFFEL TOWER', data=xml)

View File

@@ -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("""
<problem>
<p>What is height of eiffel tower without the antenna?.</p>
<multiplechoiceresponse>
<choicegroup label="What is height of eiffel tower without the antenna?" type="MultipleChoice">
<choice correct="false">324 meters<choicehint>Antenna is 24 meters high</choicehint></choice>
<choice correct="true">300 meters</choice>
<choice correct="false">224 meters</choice>
<choice correct="false">400 meters</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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: "<Subsection Title> (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: "<Subsection Title> 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: "<Subsection Title> (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())

View File

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

View File

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

View File

@@ -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 "<Role>" 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 "<Role>" 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:
<class 'xmodule.graders.WeightedSubsectionsGrader'>
Graded sections:
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Homework, category=Homework, weight=0.15
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Lab, category=Lab, weight=0.15
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, type=Midterm Exam, category=Midterm Exam, weight=0.3
subgrader=<class 'xmodule.graders.AssignmentFormatGrader'>, 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'),
('<Test>Notes</Test>', '<Test>Notes</Test>'),
)
@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)

View File

@@ -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()

View File

@@ -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 <clarification> element that can be used in problem XML.
"""
def get_problem(self):
"""
Create a problem with a <clarification>
"""
xml = dedent(u"""
<problem markdown="null">
<text>
<p>
Given the data in Table 7 <clarification>Table 7: "Example PV Installation Costs",
Page 171 of Roberts textbook</clarification>, compute the ROI
<clarification>Return on Investment <strong>(per year)</strong></clarification> over 20 years.
</p>
<numericalresponse answer="6.5">
<label>Enter the annual ROI</label>
<textline trailing_text="%" />
</numericalresponse>
</text>
</problem>
""")
return XBlockFixtureDesc('problem', 'TOOLTIP TEST PROBLEM', data=xml)
def test_clarification(self):
"""
Test that we can see the <clarification> 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("""
<problem>
<label>Which of the following countries has the largest population?</label>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint></choice>
<choice correct="false">Germany</choice>
<choice correct="true">Indonesia</choice>
<choice correct="false">Russia</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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("""
<problem>
<label>Which of the following countries has the largest population?</label>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint></choice>
<choice correct="false">Germany</choice>
<choice correct="true">Indonesia</choice>
<choice correct="false">Russia</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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("""
<problem>
<label>Which of the following countries has the largest population?</label>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint></choice>
<choice correct="false">Germany</choice>
<choice correct="true">Indonesia</choice>
<choice correct="false">Russia</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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("""
<problem>
<label>Which of the following countries has the largest population?</label>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint></choice>
<choice correct="false">Germany</choice>
<choice correct="true">Indonesia</choice>
<choice correct="false">Russia</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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("""
<problem>
<label>Which of the following countries has the largest population?</label>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint></choice>
<choice correct="false">Germany</choice>
<choice correct="true">Indonesia</choice>
<choice correct="false">Russia</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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("""
<problem>
<p>question text</p>
<stringresponse answer="A">
<stringequalhint answer="B">hint</stringequalhint>
<textline size="20"/>
</stringresponse>
<demandhint>
<hint>demand-hint1</hint>
<hint>demand-hint2</hint>
</demandhint>
</problem>
""")
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("""
<problem>
<p>question text</p>
<stringresponse answer="A">
<stringequalhint answer="C"><a href="#">aa bb</a> cc</stringequalhint>
<textline size="20"/>
</stringresponse>
<demandhint>
<hint>aa <a href="#">bb</a> cc</hint>
<hint><a href="#">dd ee</a> ff</hint>
</demandhint>
</problem>
""")
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': '<a href="#">aa bb</a> 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 <a href="#">bb</a> cc'}},
{'event': {u'hint_index': 1, u'hint_len': 2, u'hint_text': u'<a href="#">dd ee</a> ff'}}
]
)
@attr(shard=9)
class ProblemWithMathjax(ProblemsTest):
"""
Tests the <MathJax> used in problem
"""
def get_problem(self):
"""
Create a problem with a <MathJax> in body and hint
"""
xml = dedent(r"""
<problem>
<p>Check mathjax has rendered [mathjax]E=mc^2[/mathjax]</p>
<multiplechoiceresponse>
<label>Answer this?</label>
<choicegroup type="MultipleChoice">
<choice correct="true">Choice1 <choicehint>Correct choice message</choicehint></choice>
<choice correct="false">Choice2<choicehint>Wrong choice message</choicehint></choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>mathjax should work1 \(E=mc^2\) </hint>
<hint>mathjax should work2 [mathjax]E=mc^2[/mathjax]</hint>
</demandhint>
</problem>
""")
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(
["<strong>Hint (1 of 2): </strong>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(
[
"<strong>Hint (1 of 2): </strong>mathjax should work1",
"<strong>Hint (2 of 2): </strong>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("""
<problem>
<p>The answer is 1. Partial credit for -1.</p>
<numericalresponse answer="1" partial_credit="list">
<label>How many miles away from Earth is the sun? Use scientific notation to answer.</label>
<formulaequationinput/>
<responseparam type="tolerance" default="0.01" />
<responseparam partial_answers="-1" />
</numericalresponse>
</problem>
""")
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("""
<problem>
<numericalresponse answer="1">
<label>The answer is 1</label>
<formulaequationinput/>
<responseparam type="tolerance" default="0.01" />
</numericalresponse>
</problem>
""")
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"""
<problem>
<choiceresponse>
<label>Eggplant is a _____?</label>
<description>{}</description>
<description>{}</description>
<checkboxgroup>
<choice correct="true">vegetable</choice>
<choice correct="false">fruit</choice>
</checkboxgroup>
</choiceresponse>
</problem>
""".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
</formularesponse>
</problem>""")
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("""
<problem>
<label>Which of the following countries has the largest population?</label>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint></choice>
<choice correct="false">Germany</choice>
<choice correct="true">Indonesia</choice>
<choice correct="false">Russia</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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("""
<problem>
<label>Which of the following countries has the largest population?</label>
<multiplechoiceresponse>
<choicegroup type="MultipleChoice">
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint></choice>
<choice correct="false">Germany</choice>
<choice correct="true">Indonesia</choice>
<choice correct="false">Russia</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""")
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("""
<problem>
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="R_1*R_2/R_3">
<p>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.</p>
<label>Add the question text, or prompt, here. This text is required. Example: Write an expression for the product of R_1, R_2, and the inverse of R_3.</label>
<description>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</description>
<responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40"/>
</formularesponse>
</problem>
""")
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("""
<problem>
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="R_1*R_2/R_3">
<p>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.</p>
<label>Add the question text, or prompt, here. This text is required. Example: Write an expression for the product of R_1, R_2, and the inverse of R_3.</label>
<description>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</description>
<responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40"/>
</formularesponse>
</problem>
""")
# 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())

View File

@@ -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='<html>VISIBLE TO A</html>',
metadata={"group_access": {0: [0]}}
),
XBlockFixtureDesc(
'html',
'VISIBLE TO B',
data='<html>VISIBLE TO B</html>',
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]

View File

@@ -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("""
<problem markdown="Simple Problem" max_attempts="" weight="">
<p>Choose Yes.</p>
<choiceresponse>
<checkboxgroup>
<choice correct="true">Yes</choice>
</checkboxgroup>
</choiceresponse>
</problem>
""")
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)

View File

@@ -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 "<ProblemType>" problem
When I answer a "<ProblemType>" problem "correctly"
Then my "<ProblemType>" answer is marked "correct"
And The "<ProblemType>" 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 "<ProblemType>" problem
When I answer a "<ProblemType>" problem "incorrectly"
Then my "<ProblemType>" answer is marked "incorrect"
And The "<ProblemType>" 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 "<ProblemType>" problem
When I submit a problem
Then my "<ProblemType>" answer is marked "incorrect"
And The "<ProblemType>" 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 <correctness>
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 <correctness>
Then I should see a <score>
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 <initial correctness>
Then my problem's answer is marked with <initial correctness>
And I input an answer as <other correctness>
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 <Correctness>ly
Given External graders respond "<Correctness>"
And I am viewing a "<ProblemType>" 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 "<ProblemType>" problem "<Correctness>ly"
And the "<ProblemType>" 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 <correctness>
Then I should see a <score>
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 <initial_correctness>
Then my multiple choice answer is marked <initial_correctness>
And I reset the problem
Then my multiple choice answer is NOT marked <initial_correctness>
And my multiple choice answer is NOT marked <other_correctness>
"""
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 <correctness>
Then I should see a <score>
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 <initial_correctness>
Then my radio answer is marked <initial_correctness>
And I reset the problem
Then my radio problem's answer is NOT marked <initial_correctness>
And my radio problem's answer is NOT marked <other_correctness>
"""
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

View File

@@ -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

View File

@@ -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

View File

@@ -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': '<script>alert("XSS")</script>',
'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()

View File

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

View File

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

View File

@@ -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="<html>Contents</html>"),
)
)
)
)
).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()

View File

@@ -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

View File

@@ -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='<problem></problem>')
)
)
)
)
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 """
<div class="xblock xblock-student_view xmodule_display xmodule_HtmlBlock xblock-initialized"
data-runtime-class="LmsRuntime" data-init="XBlockToXModuleShim" data-block-type="html"
data-runtime-version="1" data-type="HTMLModule" data-course-id="GeorgetownX/HUMW-421-01"
data-request-token="thisIsNotARealRequestToken"
data-usage-id="i4x:;_;_GeorgetownX;_HUMW-421-01;_html;_3010cbbecaa1484da6cf8ba01362346a">
<p>Copied from LMS HTML component</p></div>
"""
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 """
<ol class="components ui-sortable">
<li class="component" data-locator="i4x://Wellesley_College/100/html/6390f1fd3fe640d49580b8415fe1330b"
data-course-key="Wellesley_College/100/2014_Summer">
<div class="xblock xblock-student_view xmodule_display xmodule_HtmlBlock xblock-initialized"
data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1"
data-request-token="thisIsNotARealRequestToken"
data-usage-id="i4x://Wellesley_College/100/html/6390f1fd3fe640d49580b8415fe1330b"
data-type="HTMLModule" data-block-type="html">
<h2>VOICE COMPARISON </h2>
<p>You can access the experimental <strong >Voice Comparison</strong> tool at the link below.</p>
</div>
</li>
</ol>
"""
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 "<script>var doesNotExist = BadGlobal.foo;</script>"

View File

@@ -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 "<Component>" "Advanced Problem" component
Then I see a "<Component>" 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)

View File

@@ -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 = '<p><strong>Body of HTML Unit.</strong></p>'
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', '<problem></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()

View File

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

View File

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

View File

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

View File

@@ -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
})

Some files were not shown because too many files have changed in this diff Show More