1179 lines
48 KiB
Python
1179 lines
48 KiB
Python
# coding: utf-8
|
|
"""
|
|
Acceptance tests for Studio's Setting pages
|
|
"""
|
|
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
import string
|
|
from textwrap import dedent
|
|
|
|
import six
|
|
from bok_choy.promise import EmptyPromise
|
|
from mock import patch
|
|
from six.moves import range
|
|
|
|
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
|
|
from common.test.acceptance.pages.common.utils import add_enrollment_course_modes
|
|
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
|
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_group_configurations import GroupConfigurationsPage
|
|
from common.test.acceptance.pages.studio.utils import get_input_value, type_in_codemirror
|
|
from common.test.acceptance.tests.helpers import create_user_partition_json, element_has_text
|
|
from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest
|
|
from openedx.core.lib.tests import attr
|
|
from xmodule.partitions.partitions import Group
|
|
|
|
|
|
@attr(shard=19)
|
|
class ContentGroupConfigurationTest(StudioCourseTest):
|
|
"""
|
|
Tests for content groups in the Group Configurations Page.
|
|
There are tests for the experiment groups in test_studio_split_test.
|
|
"""
|
|
def setUp(self):
|
|
super(ContentGroupConfigurationTest, self).setUp()
|
|
self.group_configurations_page = GroupConfigurationsPage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
|
|
self.outline_page = CourseOutlinePage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
|
|
def populate_course_fixture(self, course_fixture):
|
|
"""
|
|
Populates test course with chapter, sequential, and 1 problems.
|
|
The problem is visible only to Group "alpha".
|
|
"""
|
|
course_fixture.add_children(
|
|
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
|
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Test Unit')
|
|
)
|
|
)
|
|
)
|
|
|
|
def create_and_verify_content_group(self, name, existing_groups):
|
|
"""
|
|
Creates a new content group and verifies that it was properly created.
|
|
"""
|
|
self.assertEqual(existing_groups, len(self.group_configurations_page.content_groups))
|
|
if existing_groups == 0:
|
|
self.group_configurations_page.create_first_content_group()
|
|
else:
|
|
self.group_configurations_page.add_content_group()
|
|
config = self.group_configurations_page.content_groups[existing_groups]
|
|
config.name = name
|
|
# Save the content group
|
|
self.assertEqual(config.get_text('.action-primary'), "Create")
|
|
self.assertFalse(config.delete_button_is_present)
|
|
config.save()
|
|
self.assertIn(name, config.name)
|
|
return config
|
|
|
|
def test_no_content_groups_by_default(self):
|
|
"""
|
|
Scenario: Ensure that message telling me to create a new content group is
|
|
shown when no content groups exist.
|
|
Given I have a course without content groups
|
|
When I go to the Group Configuration page in Studio
|
|
Then I see "You have not created any content groups yet." message
|
|
"""
|
|
self.group_configurations_page.visit()
|
|
self.assertTrue(self.group_configurations_page.no_content_groups_message_is_present)
|
|
self.assertIn(
|
|
"You have not created any content groups yet.",
|
|
self.group_configurations_page.no_content_groups_message_text
|
|
)
|
|
|
|
def test_can_create_and_edit_content_groups(self):
|
|
"""
|
|
Scenario: Ensure that the content groups can be created and edited correctly.
|
|
Given I have a course without content groups
|
|
When I click button 'Add your first Content Group'
|
|
And I set new the name and click the button 'Create'
|
|
Then I see the new content is added and has correct data
|
|
And I click 'New Content Group' button
|
|
And I set the name and click the button 'Create'
|
|
Then I see the second content group is added and has correct data
|
|
When I edit the second content group
|
|
And I change the name and click the button 'Save'
|
|
Then I see the second content group is saved successfully and has the new name
|
|
"""
|
|
self.group_configurations_page.visit()
|
|
self.create_and_verify_content_group("New Content Group", 0)
|
|
second_config = self.create_and_verify_content_group("Second Content Group", 1)
|
|
|
|
# Edit the second content group
|
|
second_config.edit()
|
|
second_config.name = "Updated Second Content Group"
|
|
self.assertEqual(second_config.get_text('.action-primary'), "Save")
|
|
second_config.save()
|
|
|
|
self.assertIn("Updated Second Content Group", second_config.name)
|
|
|
|
def test_cannot_delete_used_content_group(self):
|
|
"""
|
|
Scenario: Ensure that the user cannot delete used content group.
|
|
Given I have a course with 1 Content Group
|
|
And I go to the Group Configuration page
|
|
When I try to delete the Content Group with name "New Content Group"
|
|
Then I see the delete button is disabled.
|
|
"""
|
|
self.course_fixture._update_xblock(self.course_fixture._course_location, {
|
|
"metadata": {
|
|
u"user_partitions": [
|
|
create_user_partition_json(
|
|
0,
|
|
'Configuration alpha,',
|
|
'Content Group Partition',
|
|
[Group("0", 'alpha')],
|
|
scheme="cohort"
|
|
)
|
|
],
|
|
},
|
|
})
|
|
problem_data = dedent("""
|
|
<problem markdown="Simple Problem" max_attempts="" weight="">
|
|
<p>Choose Yes.</p>
|
|
<choiceresponse>
|
|
<checkboxgroup>
|
|
<choice correct="true">Yes</choice>
|
|
</checkboxgroup>
|
|
</choiceresponse>
|
|
</problem>
|
|
""")
|
|
vertical = self.course_fixture.get_nested_xblocks(category="vertical")[0]
|
|
self.course_fixture.create_xblock(
|
|
vertical.locator,
|
|
XBlockFixtureDesc('problem', "VISIBLE TO ALPHA", data=problem_data, metadata={"group_access": {0: [0]}}),
|
|
)
|
|
self.group_configurations_page.visit()
|
|
config = self.group_configurations_page.content_groups[0]
|
|
self.assertTrue(config.delete_button_is_disabled)
|
|
|
|
def test_can_delete_unused_content_group(self):
|
|
"""
|
|
Scenario: Ensure that the user can delete unused content group.
|
|
Given I have a course with 1 Content Group
|
|
And I go to the Group Configuration page
|
|
When I delete the Content Group with name "New Content Group"
|
|
Then I see that there is no Content Group
|
|
When I refresh the page
|
|
Then I see that the content group has been deleted
|
|
"""
|
|
self.group_configurations_page.visit()
|
|
config = self.create_and_verify_content_group("New Content Group", 0)
|
|
self.assertTrue(config.delete_button_is_present)
|
|
|
|
self.assertEqual(len(self.group_configurations_page.content_groups), 1)
|
|
|
|
# Delete content group
|
|
config.delete()
|
|
self.assertEqual(len(self.group_configurations_page.content_groups), 0)
|
|
|
|
self.group_configurations_page.visit()
|
|
self.assertEqual(len(self.group_configurations_page.content_groups), 0)
|
|
|
|
def test_must_supply_name(self):
|
|
"""
|
|
Scenario: Ensure that validation of the content group works correctly.
|
|
Given I have a course without content groups
|
|
And I create new content group without specifying a name click the button 'Create'
|
|
Then I see error message "Content Group name is required."
|
|
When I set a name and click the button 'Create'
|
|
Then I see the content group is saved successfully
|
|
"""
|
|
self.group_configurations_page.visit()
|
|
self.group_configurations_page.create_first_content_group()
|
|
config = self.group_configurations_page.content_groups[0]
|
|
config.save()
|
|
self.assertEqual(config.mode, 'edit')
|
|
self.assertEqual("Group name is required", config.validation_message)
|
|
config.name = "Content Group Name"
|
|
config.save()
|
|
self.assertIn("Content Group Name", config.name)
|
|
|
|
def test_can_cancel_creation_of_content_group(self):
|
|
"""
|
|
Scenario: Ensure that creation of a content group can be canceled correctly.
|
|
Given I have a course without content groups
|
|
When I click button 'Add your first Content Group'
|
|
And I set new the name and click the button 'Cancel'
|
|
Then I see that there is no content groups in the course
|
|
"""
|
|
self.group_configurations_page.visit()
|
|
self.group_configurations_page.create_first_content_group()
|
|
config = self.group_configurations_page.content_groups[0]
|
|
config.name = "Content Group"
|
|
config.cancel()
|
|
self.assertEqual(0, len(self.group_configurations_page.content_groups))
|
|
|
|
def test_content_group_empty_usage(self):
|
|
"""
|
|
Scenario: When content group is not used, ensure that the link to outline page works correctly.
|
|
Given I have a course without content group
|
|
And I create new content group
|
|
Then I see a link to the outline page
|
|
When I click on the outline link
|
|
Then I see the outline page
|
|
"""
|
|
self.group_configurations_page.visit()
|
|
config = self.create_and_verify_content_group("New Content Group", 0)
|
|
config.toggle()
|
|
config.click_outline_anchor()
|
|
|
|
# Waiting for the page load and verify that we've landed on course outline page
|
|
self.outline_page.wait_for_page()
|
|
|
|
|
|
@attr(shard=17)
|
|
class EnrollmentTrackModeTest(StudioCourseTest):
|
|
"""
|
|
Tests for the enrollment tracks section
|
|
"""
|
|
|
|
def setUp(self, is_staff=True, test_xss=True):
|
|
super(EnrollmentTrackModeTest, self).setUp(is_staff=is_staff)
|
|
|
|
self.audit_track = "Audit"
|
|
self.verified_track = "Verified"
|
|
self.staff_user = self.user
|
|
|
|
def test_all_course_modes_present(self):
|
|
"""
|
|
This test is meant to ensure that all the course modes show up as groups
|
|
on the Group configuration page within the Enrollment Tracks section.
|
|
It also checks to make sure that the edit buttons are not available.
|
|
"""
|
|
add_enrollment_course_modes(self.browser, self.course_id, ['audit', 'verified'])
|
|
group_configurations_page = GroupConfigurationsPage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
group_configurations_page.visit()
|
|
self.assertTrue(group_configurations_page.enrollment_track_section_present)
|
|
|
|
# Make sure the edit buttons are not available.
|
|
self.assertFalse(group_configurations_page.enrollment_track_edit_present)
|
|
groups = group_configurations_page.get_enrollment_groups()
|
|
for g in [self.audit_track, self.verified_track]:
|
|
self.assertTrue(g in groups)
|
|
|
|
def test_one_course_mode(self):
|
|
"""
|
|
The purpose of this test is to ensure that when there is 1 or fewer course modes
|
|
the enrollment track section is not shown.
|
|
"""
|
|
add_enrollment_course_modes(self.browser, self.course_id, ['audit'])
|
|
group_configurations_page = GroupConfigurationsPage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
group_configurations_page.visit()
|
|
self.assertFalse(group_configurations_page.enrollment_track_section_present)
|
|
groups = group_configurations_page.get_enrollment_groups()
|
|
self.assertEqual(len(groups), 0)
|
|
|
|
|
|
@attr(shard=19)
|
|
class AdvancedSettingsValidationTest(StudioCourseTest):
|
|
"""
|
|
Tests for validation feature in Studio's advanced settings tab
|
|
"""
|
|
course_name_key = 'Course Display Name'
|
|
course_name_value = 'Test Name'
|
|
|
|
def setUp(self):
|
|
super(AdvancedSettingsValidationTest, self).setUp()
|
|
self.advanced_settings = AdvancedSettingsPage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
|
|
self.type_fields = ['Course Display Name', 'Advanced Module List', 'Discussion Topic Mapping',
|
|
'Maximum Attempts', 'Course Announcement Date']
|
|
|
|
# Before every test, make sure to visit the page first
|
|
self.advanced_settings.visit()
|
|
|
|
def test_course_author_sees_default_advanced_settings(self):
|
|
"""
|
|
Scenario: Test that advanced settings have the default settings
|
|
Given a staff logs in to studio
|
|
When this user goes to advanced settings page
|
|
Then this user sees 'Allow Anonymous Discussion Posts' as true
|
|
And 'Enable Timed Exams' as false
|
|
And 'Maximum Attempts' as null
|
|
"""
|
|
anonymous_discussion_setting = self.advanced_settings.get('Allow Anonymous Discussion Posts')
|
|
timed_exam_settings = self.advanced_settings.get('Enable Timed Exams')
|
|
max_attempts = self.advanced_settings.get('Maximum Attempts')
|
|
page_default_settings = [
|
|
anonymous_discussion_setting,
|
|
timed_exam_settings,
|
|
max_attempts
|
|
]
|
|
default_anonymous_discussion_setting = 'true'
|
|
default_timed_exam_settings = 'false'
|
|
default_max_attempts = 'null'
|
|
expected_default_settings = [
|
|
default_anonymous_discussion_setting,
|
|
default_timed_exam_settings,
|
|
default_max_attempts
|
|
]
|
|
self.assertEqual(
|
|
page_default_settings,
|
|
expected_default_settings
|
|
)
|
|
|
|
def test_keys_appear_alphabetically(self):
|
|
"""
|
|
Scenario: Test that advanced settings have all the keys in alphabetic order
|
|
Given a staff logs in to studio
|
|
When this user goes to advanced settings page
|
|
Then he sees all the advanced setting keys in alphabetic order
|
|
"""
|
|
|
|
key_names = self.advanced_settings.key_names
|
|
self.assertEqual(key_names, sorted(key_names))
|
|
|
|
def test_cancel_editing_key_value(self):
|
|
"""
|
|
Scenario: Test that advanced settings does not save the key value, if cancel
|
|
is clicked from notification bar
|
|
Given a staff logs in to studio
|
|
When this user goes to advanced settings page and enters and new course name
|
|
Then he clicks 'cancel' buttin when asked to save changes
|
|
When this user reloads the page
|
|
And then he does not see any change in the original course name
|
|
"""
|
|
|
|
original_course_display_name = self.advanced_settings.get(self.course_name_key)
|
|
new_course_name = 'New Course Name'
|
|
type_in_codemirror(self.advanced_settings, 16, new_course_name)
|
|
self.advanced_settings.cancel()
|
|
self.advanced_settings.refresh_and_wait_for_load()
|
|
self.assertNotEqual(
|
|
original_course_display_name,
|
|
new_course_name,
|
|
(
|
|
'original course name:{} can not not be equal to unsaved course name {}'.format(
|
|
original_course_display_name,
|
|
new_course_name
|
|
)
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
self.advanced_settings.get(self.course_name_key),
|
|
original_course_display_name,
|
|
(
|
|
'course name from the page should be same as original_course_display_name:{}'.format(
|
|
original_course_display_name
|
|
)
|
|
)
|
|
)
|
|
|
|
def test_editing_key_value(self):
|
|
"""
|
|
Scenario: Test that advanced settings saves the key value, if save button
|
|
is clicked from notification bar after the editing
|
|
Given a staff logs in to studio
|
|
When this user goes to advanced settings page and enters a new course name
|
|
And he clicks 'save' button from the notification bar
|
|
Then he is able to see the updated course name
|
|
"""
|
|
new_course_name = ''.join(random.choice(string.ascii_uppercase) for _ in range(10))
|
|
self.advanced_settings.set(self.course_name_key, new_course_name)
|
|
self.assertEqual(
|
|
self.advanced_settings.get(self.course_name_key),
|
|
'"{}"'.format(new_course_name),
|
|
(
|
|
'course name from the page should be same as new_course_name:{}'.format(
|
|
new_course_name
|
|
)
|
|
)
|
|
)
|
|
|
|
def test_confirmation_is_shown_on_save(self):
|
|
"""
|
|
Scenario: Test that advanced settings shows confirmation after editing a field successfully
|
|
Given a staff logs in to studio
|
|
When this user goes to advanced settings page and edits any value
|
|
And he clicks 'save' button from the notification bar
|
|
Then he is able to see the confirmation message
|
|
"""
|
|
self.advanced_settings.set('Maximum Attempts', 5)
|
|
confirmation_message = self.advanced_settings.confirmation_message
|
|
self.assertEqual(
|
|
confirmation_message,
|
|
'Your policy changes have been saved.',
|
|
'Settings must be saved successfully in order to have confirmation message'
|
|
)
|
|
|
|
def test_deprecated_settings_can_be_toggled(self):
|
|
"""
|
|
Scenario: Test that advanced settings can toggle deprecated settings
|
|
Given I am on the Advanced Course Settings page in Studio
|
|
When I toggle the display of deprecated settings
|
|
Then deprecated settings are then shown
|
|
And I toggle the display of deprecated settings
|
|
Then deprecated settings are not shown
|
|
"""
|
|
|
|
self.advanced_settings.toggle_deprecated_settings()
|
|
button_text = self.advanced_settings.deprecated_settings_button_text
|
|
self.assertEqual(
|
|
button_text,
|
|
'Hide Deprecated Settings',
|
|
"Button text should change to 'Hide Deprecated Settings' after the click"
|
|
)
|
|
self.assertTrue(self.advanced_settings.is_deprecated_setting_visible())
|
|
self.advanced_settings.toggle_deprecated_settings()
|
|
self.assertFalse(self.advanced_settings.is_deprecated_setting_visible())
|
|
self.assertEqual(
|
|
self.advanced_settings.deprecated_settings_button_text,
|
|
'Show Deprecated Settings',
|
|
"Button text should change to 'Show Deprecated Settings' after the click"
|
|
)
|
|
|
|
def test_multi_line_input(self):
|
|
"""
|
|
Scenario: Test that advanced settings correctly shows the multi-line input
|
|
Given I am on the Advanced Course Settings page in Studio
|
|
When I create a JSON object as a value for "Discussion Topic Mapping"
|
|
Then it is displayed as formatted
|
|
"""
|
|
|
|
inputs = {
|
|
"key": "value",
|
|
"key_2": "value_2"
|
|
}
|
|
json_input = json.dumps(inputs)
|
|
self.advanced_settings.set('Discussion Topic Mapping', json_input)
|
|
assert self.advanced_settings.get('Discussion Topic Mapping') in (
|
|
'{\n "key": "value",\n "key_2": "value_2"\n}',
|
|
'{\n "key_2": "value_2",\n "key": "value"\n}'
|
|
)
|
|
|
|
def test_automatic_quoting_of_non_json_value(self):
|
|
"""
|
|
Scenario: Test that advanced settings automatically quotes the field input
|
|
upon saving
|
|
Given I am on the Advanced Course Settings page in Studio
|
|
When I create a non-JSON value not in quotes
|
|
Then it is displayed as a string
|
|
"""
|
|
|
|
self.advanced_settings.set(self.course_name_key, self.course_name_value)
|
|
self.assertEqual(
|
|
self.advanced_settings.get(self.course_name_key),
|
|
'"Test Name"'
|
|
)
|
|
|
|
def test_validation_error_for_wrong_input_type(self):
|
|
"""
|
|
Scenario: Test error if value supplied is of the wrong type
|
|
Given I am on the Advanced Course Settings page in Studio
|
|
When I create a JSON object as a value for "Course Display Name"
|
|
Then I get an error on save
|
|
And I reload the page
|
|
Then the policy key value is unchanged
|
|
"""
|
|
|
|
course_display_name = self.advanced_settings.get('Course Display Name')
|
|
inputs = {
|
|
"key": "value",
|
|
"key_2": "value_2"
|
|
}
|
|
json_input = json.dumps(inputs)
|
|
self.advanced_settings.set('Course Display Name', json_input)
|
|
self.advanced_settings.wait_for_modal_load()
|
|
self.check_modal_shows_correct_contents(['Course Display Name'])
|
|
self.advanced_settings.refresh_and_wait_for_load()
|
|
self.assertEqual(
|
|
self.advanced_settings.get('Course Display Name'),
|
|
course_display_name,
|
|
'Wrong input for Course Display Name must not change its value'
|
|
)
|
|
|
|
def test_modal_shows_one_validation_error(self):
|
|
"""
|
|
Test that advanced settings don't save if there's a single wrong input,
|
|
and that it shows the correct error message in the modal.
|
|
"""
|
|
|
|
# Feed an integer value for String field.
|
|
# .set method saves automatically after setting a value
|
|
course_display_name = self.advanced_settings.get('Course Display Name')
|
|
self.advanced_settings.set('Course Display Name', 1)
|
|
self.advanced_settings.wait_for_modal_load()
|
|
|
|
# Test Modal
|
|
self.check_modal_shows_correct_contents(['Course Display Name'])
|
|
self.advanced_settings.refresh_and_wait_for_load()
|
|
|
|
self.assertEqual(
|
|
self.advanced_settings.get('Course Display Name'),
|
|
course_display_name,
|
|
'Wrong input for Course Display Name must not change its value'
|
|
)
|
|
|
|
def test_modal_shows_multiple_validation_errors(self):
|
|
"""
|
|
Test that advanced settings don't save with multiple wrong inputs
|
|
"""
|
|
|
|
# Save original values and feed wrong inputs
|
|
original_values_map = self.get_settings_fields_of_each_type()
|
|
self.set_wrong_inputs_to_fields()
|
|
self.advanced_settings.wait_for_modal_load()
|
|
|
|
# Test Modal
|
|
self.check_modal_shows_correct_contents(self.type_fields)
|
|
self.advanced_settings.refresh_and_wait_for_load()
|
|
|
|
for key, val in six.iteritems(original_values_map):
|
|
self.assertEqual(
|
|
self.advanced_settings.get(key),
|
|
val,
|
|
'Wrong input for Advanced Settings Fields must not change its value'
|
|
)
|
|
|
|
def test_undo_changes(self):
|
|
"""
|
|
Test that undo changes button in the modal resets all settings changes
|
|
"""
|
|
|
|
# Save original values and feed wrong inputs
|
|
original_values_map = self.get_settings_fields_of_each_type()
|
|
self.set_wrong_inputs_to_fields()
|
|
|
|
# Let modal popup
|
|
self.advanced_settings.wait_for_modal_load()
|
|
|
|
# Click Undo Changes button
|
|
self.advanced_settings.undo_changes_via_modal()
|
|
|
|
# Check that changes are undone
|
|
for key, val in six.iteritems(original_values_map):
|
|
self.assertEqual(
|
|
self.advanced_settings.get(key),
|
|
val,
|
|
'Undoing Should revert back to original value'
|
|
)
|
|
|
|
def test_manual_change(self):
|
|
"""
|
|
Test that manual changes button in the modal keeps settings unchanged
|
|
"""
|
|
inputs = {"Course Display Name": 1,
|
|
"Advanced Module List": 1,
|
|
"Discussion Topic Mapping": 1,
|
|
"Maximum Attempts": '"string"',
|
|
"Course Announcement Date": '"string"',
|
|
}
|
|
|
|
self.set_wrong_inputs_to_fields()
|
|
self.advanced_settings.wait_for_modal_load()
|
|
self.advanced_settings.trigger_manual_changes()
|
|
|
|
# Check that the validation modal went away.
|
|
self.assertFalse(self.advanced_settings.is_validation_modal_present())
|
|
|
|
# Iterate through the wrong values and make sure they're still displayed
|
|
for key, val in six.iteritems(inputs):
|
|
self.assertEqual(
|
|
str(self.advanced_settings.get(key)),
|
|
str(val),
|
|
'manual change should keep: ' + str(val) + ', but is: ' + str(self.advanced_settings.get(key))
|
|
)
|
|
|
|
def check_modal_shows_correct_contents(self, wrong_settings_list):
|
|
"""
|
|
Helper function that checks if the validation modal contains correct
|
|
error messages.
|
|
"""
|
|
# Check presence of modal
|
|
self.assertTrue(self.advanced_settings.is_validation_modal_present())
|
|
|
|
# List of wrong settings item & what is presented in the modal should be the same
|
|
error_item_names = self.advanced_settings.get_error_item_names()
|
|
self.assertEqual(set(wrong_settings_list), set(error_item_names))
|
|
|
|
error_item_messages = self.advanced_settings.get_error_item_messages()
|
|
self.assertEqual(len(error_item_names), len(error_item_messages))
|
|
|
|
def get_settings_fields_of_each_type(self):
|
|
"""
|
|
Get one of each field type:
|
|
- String: Course Display Name
|
|
- List: Advanced Module List
|
|
- Dict: Discussion Topic Mapping
|
|
- Integer: Maximum Attempts
|
|
- Date: Course Announcement Date
|
|
"""
|
|
return {
|
|
"Course Display Name": self.advanced_settings.get('Course Display Name'),
|
|
"Advanced Module List": self.advanced_settings.get('Advanced Module List'),
|
|
"Discussion Topic Mapping": self.advanced_settings.get('Discussion Topic Mapping'),
|
|
"Maximum Attempts": self.advanced_settings.get('Maximum Attempts'),
|
|
"Course Announcement Date": self.advanced_settings.get('Course Announcement Date'),
|
|
}
|
|
|
|
def set_wrong_inputs_to_fields(self):
|
|
"""
|
|
Set wrong values for the chosen fields
|
|
"""
|
|
self.advanced_settings.set_values(
|
|
{
|
|
"Course Display Name": 1,
|
|
"Advanced Module List": 1,
|
|
"Discussion Topic Mapping": 1,
|
|
"Maximum Attempts": '"string"',
|
|
"Course Announcement Date": '"string"',
|
|
}
|
|
)
|
|
|
|
def test_only_expected_fields_are_displayed(self):
|
|
"""
|
|
Scenario: The Advanced Settings screen displays settings/fields not specifically hidden from
|
|
view by a developer.
|
|
Given I have a set of CourseMetadata fields defined for the course
|
|
When I view the Advanced Settings screen for the course
|
|
The total number of fields displayed matches the number I expect
|
|
And the actual fields displayed match the fields I expect to see
|
|
"""
|
|
expected_fields = self.advanced_settings.expected_settings_names
|
|
displayed_fields = self.advanced_settings.displayed_settings_names
|
|
self.assertEqual(set(displayed_fields), set(expected_fields))
|
|
|
|
|
|
@attr(shard=16)
|
|
class ContentLicenseTest(StudioCourseTest):
|
|
"""
|
|
Tests for course-level licensing (that is, setting the license,
|
|
for an entire course's content, to All Rights Reserved or Creative Commons)
|
|
"""
|
|
def setUp(self): # pylint: disable=arguments-differ
|
|
super(ContentLicenseTest, self).setUp()
|
|
self.outline_page = CourseOutlinePage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
self.settings_page = SettingsPage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
self.lms_courseware = CoursewarePage(
|
|
self.browser,
|
|
self.course_id,
|
|
)
|
|
self.settings_page.visit()
|
|
|
|
def test_empty_license(self):
|
|
"""
|
|
When I visit the Studio settings page,
|
|
I see that the course license is "All Rights Reserved" by default.
|
|
Then I visit the LMS courseware page,
|
|
and I see that the default course license is displayed.
|
|
"""
|
|
self.assertEqual(self.settings_page.course_license, "All Rights Reserved")
|
|
self.lms_courseware.visit()
|
|
self.assertEqual(self.lms_courseware.course_license, "© All Rights Reserved")
|
|
|
|
def test_arr_license(self):
|
|
"""
|
|
When I visit the Studio settings page,
|
|
and I set the course license to "All Rights Reserved",
|
|
and I refresh the page,
|
|
I see that the course license is "All Rights Reserved".
|
|
Then I visit the LMS courseware page,
|
|
and I see that the course license is "All Rights Reserved".
|
|
"""
|
|
self.settings_page.course_license = "All Rights Reserved"
|
|
self.settings_page.save_changes()
|
|
self.settings_page.refresh_and_wait_for_load()
|
|
self.assertEqual(self.settings_page.course_license, "All Rights Reserved")
|
|
|
|
self.lms_courseware.visit()
|
|
self.assertEqual(self.lms_courseware.course_license, "© All Rights Reserved")
|
|
|
|
def test_cc_license(self):
|
|
"""
|
|
When I visit the Studio settings page,
|
|
and I set the course license to "Creative Commons",
|
|
and I refresh the page,
|
|
I see that the course license is "Creative Commons".
|
|
Then I visit the LMS courseware page,
|
|
and I see that the course license is "Some Rights Reserved".
|
|
"""
|
|
self.settings_page.course_license = "Creative Commons"
|
|
self.settings_page.save_changes()
|
|
self.settings_page.refresh_and_wait_for_load()
|
|
self.assertEqual(self.settings_page.course_license, "Creative Commons")
|
|
|
|
self.lms_courseware.visit()
|
|
# The course_license text will include a bunch of screen reader text to explain
|
|
# the selected options
|
|
self.assertIn("Some Rights Reserved", self.lms_courseware.course_license)
|
|
|
|
|
|
@attr('a11y')
|
|
class StudioSettingsA11yTest(StudioCourseTest):
|
|
|
|
"""
|
|
Class to test Studio pages accessibility.
|
|
"""
|
|
|
|
def setUp(self): # pylint: disable=arguments-differ
|
|
super(StudioSettingsA11yTest, self).setUp()
|
|
self.settings_page = SettingsPage(self.browser, self.course_info['org'], self.course_info['number'],
|
|
self.course_info['run'])
|
|
|
|
def test_studio_settings_page_a11y(self):
|
|
"""
|
|
Check accessibility of SettingsPage.
|
|
"""
|
|
self.settings_page.visit()
|
|
self.settings_page.wait_for_page()
|
|
|
|
self.settings_page.a11y_audit.config.set_rules({
|
|
"ignore": [
|
|
'link-href', # TODO: AC-590
|
|
'aria-allowed-role', # TODO: AC-936
|
|
'landmark-complementary-is-top-level', # TODO: AC-939
|
|
'radiogroup', # TODO: AC-941
|
|
'region', # TODO: AC-932
|
|
],
|
|
})
|
|
|
|
self.settings_page.a11y_audit.check_for_accessibility_errors()
|
|
|
|
|
|
@attr('a11y')
|
|
class StudioSubsectionSettingsA11yTest(StudioCourseTest):
|
|
"""
|
|
Class to test accessibility on the subsection settings modals.
|
|
"""
|
|
|
|
def setUp(self): # pylint: disable=arguments-differ
|
|
browser = os.environ.get('SELENIUM_BROWSER', 'firefox')
|
|
|
|
# This test will fail if run using phantomjs < 2.0, due to an issue with bind()
|
|
# See https://github.com/ariya/phantomjs/issues/10522 for details.
|
|
|
|
# The course_outline uses this function, and as such will not fully load when run
|
|
# under phantomjs 1.9.8. So, to prevent this test from timing out at course_outline.visit(),
|
|
# force the use of firefox vs the standard a11y test usage of phantomjs 1.9.8.
|
|
|
|
# TODO: remove this block once https://openedx.atlassian.net/browse/TE-1047 is resolved.
|
|
if browser == 'phantomjs':
|
|
browser = 'firefox'
|
|
|
|
with patch.dict(os.environ, {'SELENIUM_BROWSER': browser}):
|
|
super(StudioSubsectionSettingsA11yTest, self).setUp(is_staff=True)
|
|
|
|
self.course_outline = CourseOutlinePage(
|
|
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({
|
|
"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')
|
|
)
|
|
)
|
|
)
|
|
|
|
def test_special_exams_menu_a11y(self):
|
|
"""
|
|
Given that I am a staff member
|
|
And I am editing settings on the special exams menu
|
|
Then that menu is accessible
|
|
"""
|
|
self.course_outline.visit()
|
|
self.course_outline.open_subsection_settings_dialog()
|
|
self.course_outline.select_advanced_tab()
|
|
|
|
self.course_outline.a11y_audit.config.set_rules({
|
|
"ignore": [
|
|
'section', # TODO: AC-491
|
|
],
|
|
})
|
|
# limit the scope of the audit to the special exams tab on the modal dialog
|
|
self.course_outline.a11y_audit.config.set_scope(
|
|
include=['section.edit-settings-timed-examination']
|
|
)
|
|
self.course_outline.a11y_audit.check_for_accessibility_errors()
|
|
|
|
|
|
@attr(shard=16)
|
|
class StudioSettingsImageUploadTest(StudioCourseTest):
|
|
"""
|
|
Class to test course settings image uploads.
|
|
"""
|
|
def setUp(self): # pylint: disable=arguments-differ
|
|
super(StudioSettingsImageUploadTest, self).setUp()
|
|
self.settings_page = SettingsPage(self.browser, self.course_info['org'], self.course_info['number'],
|
|
self.course_info['run'])
|
|
self.settings_page.visit()
|
|
|
|
# Ensure jquery is loaded before running a jQuery
|
|
self.settings_page.wait_for_ajax()
|
|
# This text appears towards the end of the work that jQuery is performing on the page
|
|
self.settings_page.wait_for_jquery_value('input#course-name:text', 'test_run')
|
|
|
|
def test_upload_course_card_image(self):
|
|
|
|
# upload image
|
|
file_to_upload = 'image.jpg'
|
|
self.settings_page.upload_image('#upload-course-image', file_to_upload)
|
|
self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#course-image'))
|
|
|
|
def test_upload_course_banner_image(self):
|
|
|
|
# upload image
|
|
file_to_upload = 'image.jpg'
|
|
self.settings_page.upload_image('#upload-banner-image', file_to_upload)
|
|
self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#banner-image'))
|
|
|
|
def test_upload_course_video_thumbnail_image(self):
|
|
|
|
# upload image
|
|
file_to_upload = 'image.jpg'
|
|
self.settings_page.upload_image('#upload-video-thumbnail-image', file_to_upload)
|
|
self.assertIn(file_to_upload, self.settings_page.get_uploaded_image_path('#video-thumbnail-image'))
|
|
|
|
|
|
@attr(shard=16)
|
|
class CourseSettingsTest(StudioCourseTest):
|
|
"""
|
|
Class to test course settings.
|
|
"""
|
|
COURSE_START_DATE_CSS = "#course-start-date"
|
|
COURSE_END_DATE_CSS = "#course-end-date"
|
|
ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date"
|
|
ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date"
|
|
|
|
COURSE_START_TIME_CSS = "#course-start-time"
|
|
COURSE_END_TIME_CSS = "#course-end-time"
|
|
ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time"
|
|
ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time"
|
|
|
|
course_start_date = '12/20/2013'
|
|
course_end_date = '12/26/2013'
|
|
enrollment_start_date = '12/01/2013'
|
|
enrollment_end_date = '12/10/2013'
|
|
|
|
dummy_time = "15:30"
|
|
|
|
def setUp(self, is_staff=False, test_xss=True):
|
|
super(CourseSettingsTest, self).setUp()
|
|
|
|
self.settings_page = SettingsPage(
|
|
self.browser,
|
|
self.course_info['org'],
|
|
self.course_info['number'],
|
|
self.course_info['run']
|
|
)
|
|
|
|
# Before every test, make sure to visit the page first
|
|
self.settings_page.visit()
|
|
self.ensure_input_fields_are_loaded()
|
|
|
|
def set_course_dates(self):
|
|
"""
|
|
Set dates for the course.
|
|
"""
|
|
dates_dictionary = {
|
|
self.COURSE_START_DATE_CSS: self.course_start_date,
|
|
self.COURSE_END_DATE_CSS: self.course_end_date,
|
|
self.ENROLLMENT_START_DATE_CSS: self.enrollment_start_date,
|
|
self.ENROLLMENT_END_DATE_CSS: self.enrollment_end_date
|
|
}
|
|
|
|
self.settings_page.set_element_values(dates_dictionary)
|
|
|
|
def ensure_input_fields_are_loaded(self):
|
|
"""
|
|
Ensures values in input fields are loaded.
|
|
"""
|
|
EmptyPromise(
|
|
lambda: self.settings_page.q(css='#course-organization').attrs('value')[0],
|
|
"Waiting for input fields to be loaded"
|
|
).fulfill()
|
|
|
|
def test_user_can_set_course_date(self):
|
|
"""
|
|
Scenario: User can set course dates
|
|
Given I have opened a new course in Studio
|
|
When I select Schedule and Details
|
|
And I set course dates
|
|
And I press the "Save" notification button
|
|
And I reload the page
|
|
Then I see the set dates
|
|
"""
|
|
|
|
# Set dates
|
|
self.set_course_dates()
|
|
# Set times
|
|
time_dictionary = {
|
|
self.COURSE_START_TIME_CSS: self.dummy_time,
|
|
self.ENROLLMENT_END_TIME_CSS: self.dummy_time
|
|
}
|
|
self.settings_page.set_element_values(time_dictionary)
|
|
# Save changes
|
|
self.settings_page.save_changes()
|
|
self.settings_page.refresh_and_wait_for_load()
|
|
self.ensure_input_fields_are_loaded()
|
|
css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS,
|
|
self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS,
|
|
self.COURSE_START_TIME_CSS, self.ENROLLMENT_END_TIME_CSS]
|
|
|
|
expected_values = [self.course_start_date, self.course_end_date,
|
|
self.enrollment_start_date, self.enrollment_end_date,
|
|
self.dummy_time, self.dummy_time]
|
|
# Assert changes have been persistent.
|
|
self.assertEqual(
|
|
[get_input_value(self.settings_page, css_selector) for css_selector in css_selectors],
|
|
expected_values
|
|
)
|
|
|
|
def test_clear_previously_set_course_dates(self):
|
|
"""
|
|
Scenario: User can clear previously set course dates (except start date)
|
|
Given I have set course dates
|
|
And I clear all the dates except start
|
|
And I press the "Save" notification button
|
|
And I reload the page
|
|
Then I see cleared dates
|
|
"""
|
|
|
|
# Set dates
|
|
self.set_course_dates()
|
|
# Clear all dates except start date
|
|
values_to_set = {
|
|
self.COURSE_END_DATE_CSS: '',
|
|
self.ENROLLMENT_START_DATE_CSS: '',
|
|
self.ENROLLMENT_END_DATE_CSS: ''
|
|
}
|
|
self.settings_page.set_element_values(values_to_set)
|
|
# Save changes and refresh the page
|
|
self.settings_page.save_changes()
|
|
self.settings_page.refresh_and_wait_for_load()
|
|
self.ensure_input_fields_are_loaded()
|
|
css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS,
|
|
self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS]
|
|
|
|
expected_values = [self.course_start_date, '', '', '']
|
|
# Assert changes have been persistent.
|
|
self.assertEqual(
|
|
[get_input_value(self.settings_page, css_selector) for css_selector in css_selectors],
|
|
expected_values
|
|
)
|
|
|
|
def test_cannot_clear_the_course_start_date(self):
|
|
"""
|
|
Scenario: User cannot clear the course start date
|
|
Given I have set course dates
|
|
And I press the "Save" notification button
|
|
And I clear the course start date
|
|
Then I receive a warning about course start date
|
|
And I reload the page
|
|
And the previously set start date is shown
|
|
"""
|
|
# Set dates
|
|
self.set_course_dates()
|
|
# Save changes
|
|
self.settings_page.save_changes()
|
|
# Get default start date
|
|
default_start_date = get_input_value(self.settings_page, self.COURSE_START_DATE_CSS)
|
|
# Set course start date to empty
|
|
self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: ''})
|
|
# Make sure error message is show with appropriate message
|
|
error_message_css = '.message-error'
|
|
self.settings_page.wait_for_element_presence(error_message_css, 'Error message is present')
|
|
self.assertEqual(element_has_text(self.settings_page, error_message_css,
|
|
"The course must have an assigned start date."), True)
|
|
# Refresh the page and assert start date has not changed.
|
|
self.settings_page.refresh_and_wait_for_load()
|
|
self.ensure_input_fields_are_loaded()
|
|
self.assertEqual(
|
|
get_input_value(self.settings_page, self.COURSE_START_DATE_CSS),
|
|
default_start_date
|
|
)
|
|
|
|
def test_user_can_correct_course_start_date_warning(self):
|
|
"""
|
|
Scenario: User can correct the course start date warning
|
|
Given I have tried to clear the course start
|
|
And I have entered a new course start date
|
|
And I press the "Save" notification button
|
|
Then The warning about course start date goes away
|
|
And I reload the page
|
|
Then my new course start date is shown
|
|
"""
|
|
# Set course start date to empty
|
|
self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: ''})
|
|
# Make sure we get error message
|
|
error_message_css = '.message-error'
|
|
self.settings_page.wait_for_element_presence(error_message_css, 'Error message is present')
|
|
self.assertEqual(element_has_text(self.settings_page, error_message_css,
|
|
"The course must have an assigned start date."), True)
|
|
# Set new course start value
|
|
self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: self.course_start_date})
|
|
self.settings_page.un_focus_input_field()
|
|
# Error message disappears
|
|
self.settings_page.wait_for_element_absence(error_message_css, 'Error message is not present')
|
|
# Save the changes and refresh the page.
|
|
self.settings_page.save_changes()
|
|
self.settings_page.refresh_and_wait_for_load()
|
|
self.ensure_input_fields_are_loaded()
|
|
# Assert changes are persistent.
|
|
self.assertEqual(
|
|
get_input_value(self.settings_page, self.COURSE_START_DATE_CSS),
|
|
self.course_start_date
|
|
)
|
|
|
|
def test_settings_are_only_persisted_when_saved(self):
|
|
"""
|
|
Scenario: Settings are only persisted when saved
|
|
Given I have set course dates
|
|
And I press the "Save" notification button
|
|
When I change fields
|
|
And I reload the page
|
|
Then I do not see the changes
|
|
"""
|
|
# Set course dates.
|
|
self.set_course_dates()
|
|
# Save changes.
|
|
self.settings_page.save_changes()
|
|
default_value_enrollment_start_date = get_input_value(self.settings_page,
|
|
self.ENROLLMENT_START_TIME_CSS)
|
|
# Set the value of enrollment start time and
|
|
# reload the page without saving.
|
|
self.settings_page.set_element_values({self.ENROLLMENT_START_TIME_CSS: self.dummy_time})
|
|
self.settings_page.refresh_and_wait_for_load()
|
|
self.ensure_input_fields_are_loaded()
|
|
|
|
css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS,
|
|
self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS,
|
|
self.ENROLLMENT_START_TIME_CSS]
|
|
|
|
expected_values = [self.course_start_date, self.course_end_date,
|
|
self.enrollment_start_date, self.enrollment_end_date,
|
|
default_value_enrollment_start_date]
|
|
# Assert that value of enrolment start time
|
|
# is not saved.
|
|
self.assertEqual(
|
|
[get_input_value(self.settings_page, css_selector) for css_selector in css_selectors],
|
|
expected_values
|
|
)
|
|
|
|
def test_settings_are_reset_on_cancel(self):
|
|
"""
|
|
Scenario: Settings are reset on cancel
|
|
Given I have set course dates
|
|
And I press the "Save" notification button
|
|
When I change fields
|
|
And I press the "Cancel" notification button
|
|
Then I do not see the changes
|
|
"""
|
|
# Set course date
|
|
self.set_course_dates()
|
|
# Save changes
|
|
self.settings_page.save_changes()
|
|
default_value_enrollment_start_date = get_input_value(self.settings_page,
|
|
self.ENROLLMENT_START_TIME_CSS)
|
|
# Set value but don't save it.
|
|
self.settings_page.set_element_values({self.ENROLLMENT_START_TIME_CSS: self.dummy_time})
|
|
self.settings_page.click_button("cancel")
|
|
# Make sure changes are not saved after cancel.
|
|
css_selectors = [self.COURSE_START_DATE_CSS, self.COURSE_END_DATE_CSS,
|
|
self.ENROLLMENT_START_DATE_CSS, self.ENROLLMENT_END_DATE_CSS,
|
|
self.ENROLLMENT_START_TIME_CSS]
|
|
|
|
expected_values = [self.course_start_date, self.course_end_date,
|
|
self.enrollment_start_date, self.enrollment_end_date,
|
|
default_value_enrollment_start_date]
|
|
|
|
self.assertEqual(
|
|
[get_input_value(self.settings_page, css_selector) for css_selector in css_selectors],
|
|
expected_values
|
|
)
|
|
|
|
def test_confirmation_is_shown_on_save(self):
|
|
"""
|
|
Scenario: Confirmation is shown on save
|
|
Given I have opened a new course in Studio
|
|
When I select Schedule and Details
|
|
And I change the "<field>" field to "<value>"
|
|
And I press the "Save" notification button
|
|
Then I see a confirmation that my changes have been saved
|
|
"""
|
|
# Set date
|
|
self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: self.course_start_date})
|
|
# Confirmation is showed upon save.
|
|
# Save_changes function ensures that save
|
|
# confirmation is shown.
|
|
self.settings_page.save_changes()
|
|
|
|
def test_changes_in_course_overview_show_a_confirmation(self):
|
|
"""
|
|
Scenario: Changes in Course Overview show a confirmation
|
|
Given I have opened a new course in Studio
|
|
When I select Schedule and Details
|
|
And I change the course overview
|
|
And I press the "Save" notification button
|
|
Then I see a confirmation that my changes have been saved
|
|
"""
|
|
# Change the value of course overview
|
|
self.settings_page.change_course_description('Changed overview')
|
|
# Save changes
|
|
# Save_changes function ensures that save
|
|
# confirmation is shown.
|
|
self.settings_page.save_changes()
|
|
|
|
def test_user_cannot_save_invalid_settings(self):
|
|
"""
|
|
Scenario: User cannot save invalid settings
|
|
Given I have opened a new course in Studio
|
|
When I select Schedule and Details
|
|
And I change the "Course Start Date" field to ""
|
|
Then the save notification button is disabled
|
|
"""
|
|
# Change the course start date to invalid date.
|
|
self.settings_page.set_element_values({self.COURSE_START_DATE_CSS: ''})
|
|
# Confirm that save button is disabled.
|
|
self.assertEqual(self.settings_page.is_element_present(".action-primary.action-save.is-disabled"), True)
|