Merge remote-tracking branch 'edx/master' into opaque-keys
Conflicts: cms/djangoapps/contentstore/tests/test_contentstore.py cms/djangoapps/contentstore/views/component.py cms/djangoapps/contentstore/views/item.py cms/djangoapps/contentstore/views/preview.py cms/djangoapps/contentstore/views/tests/test_container.py cms/static/js/spec/views/unit_spec.js cms/static/js/utils/module.js cms/templates/container.html cms/templates/studio_vertical_wrapper.html cms/templates/studio_xblock_wrapper.html common/djangoapps/student/views.py lms/templates/notes.html lms/templates/textannotation.html lms/templates/videoannotation.html
This commit is contained in:
@@ -65,7 +65,9 @@ class AutoAuthPage(PageObject):
|
||||
return url
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return True
|
||||
message = self.q(css='BODY').text[0]
|
||||
match = re.search(r'Logged in user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
|
||||
return True if match else False
|
||||
|
||||
def get_user_id(self):
|
||||
"""
|
||||
|
||||
@@ -86,5 +86,7 @@ class DashboardPage(PageObject):
|
||||
|
||||
def _changed_lang_promise(self, code):
|
||||
def _check_func():
|
||||
return self.q(css='select[name="language"] option[value="{}"]'.format(code)).selected
|
||||
return EmptyPromise(_check_func, "language changed")
|
||||
language_is_selected = self.q(css='select[name="language"] option[value="{}"]'.format(code)).selected
|
||||
modal_is_visible = self.q(css='section#change_language.modal').visible
|
||||
return (language_is_selected and not modal_is_visible)
|
||||
return EmptyPromise(_check_func, "language changed and modal hidden")
|
||||
|
||||
@@ -217,6 +217,20 @@ class VideoPage(PageObject):
|
||||
selector = self.get_element_selector(video_display_name, CSS_CLASS_NAMES['error_message'])
|
||||
return self.q(css=selector).visible
|
||||
|
||||
def is_spinner_shown(self, video_display_name=None):
|
||||
"""
|
||||
Checks if video spinner shown.
|
||||
|
||||
Arguments:
|
||||
video_display_name (str or None): Display name of a Video.
|
||||
|
||||
Returns:
|
||||
bool: Tells about spinner visibility.
|
||||
|
||||
"""
|
||||
selector = self.get_element_selector(video_display_name, CSS_CLASS_NAMES['video_spinner'])
|
||||
return self.q(css=selector).visible
|
||||
|
||||
@property
|
||||
def error_message_text(self, video_display_name=None):
|
||||
"""
|
||||
|
||||
@@ -65,7 +65,9 @@ class AutoAuthPage(PageObject):
|
||||
return url
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return True
|
||||
message = self.q(css='BODY').text[0]
|
||||
match = re.search(r'Logged in user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
|
||||
return True if match else False
|
||||
|
||||
def get_user_id(self):
|
||||
message = self.q(css='BODY').text[0].strip()
|
||||
|
||||
@@ -8,6 +8,9 @@ from . import BASE_URL
|
||||
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
from utils import click_css, wait_for_notification
|
||||
|
||||
|
||||
class ContainerPage(PageObject):
|
||||
"""
|
||||
Container page in Studio
|
||||
@@ -45,23 +48,49 @@ class ContainerPage(PageObject):
|
||||
return self.q(css=XBlockWrapper.BODY_SELECTOR).map(
|
||||
lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results
|
||||
|
||||
def drag(self, source_index, target_index, after=True):
|
||||
def drag(self, source_index, target_index):
|
||||
"""
|
||||
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 AFTER the
|
||||
one with the target_index drag handle, unless 'after' is set to False.
|
||||
This should drag the element with the source_index drag handle BEFORE the
|
||||
one with the target_index drag handle.
|
||||
"""
|
||||
draggables = self.q(css='.drag-handle')
|
||||
source = draggables[source_index]
|
||||
target = draggables[target_index]
|
||||
action = ActionChains(self.browser)
|
||||
action.click_and_hold(source).perform() # pylint: disable=protected-access
|
||||
action.move_to_element_with_offset(
|
||||
target, 0, target.size['height'] / 2 if after else 0
|
||||
).perform() # pylint: disable=protected-access
|
||||
action.release().perform()
|
||||
# When dragging before the target element, must take into account that the placeholder
|
||||
# will appear in the place where the target used to be.
|
||||
placeholder_height = 40
|
||||
action.click_and_hold(source).move_to_element_with_offset(
|
||||
target, 0, placeholder_height
|
||||
).release().perform()
|
||||
wait_for_notification(self)
|
||||
|
||||
def add_discussion(self, menu_index):
|
||||
"""
|
||||
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).
|
||||
"""
|
||||
click_css(self, 'a>span.large-discussion-icon', menu_index)
|
||||
|
||||
def duplicate(self, source_index):
|
||||
"""
|
||||
Duplicate the item with index source_index (based on vertical placement in page).
|
||||
"""
|
||||
click_css(self, 'a.duplicate-button', source_index)
|
||||
|
||||
def delete(self, source_index):
|
||||
"""
|
||||
Delete the item with index source_index (based on vertical placement in page).
|
||||
"""
|
||||
click_css(self, 'a.delete-button', source_index, require_notification=False)
|
||||
# Click the confirmation dialog button
|
||||
click_css(self, 'a.button.action-primary', 0)
|
||||
|
||||
|
||||
|
||||
class XBlockWrapper(PageObject):
|
||||
@@ -69,7 +98,7 @@ class XBlockWrapper(PageObject):
|
||||
A PageObject representing a wrapper around an XBlock child shown on the Studio container page.
|
||||
"""
|
||||
url = None
|
||||
BODY_SELECTOR = '.wrapper-xblock'
|
||||
BODY_SELECTOR = '.studio-xblock-wrapper'
|
||||
NAME_SELECTOR = '.header-details'
|
||||
|
||||
def __init__(self, browser, locator):
|
||||
|
||||
35
common/test/acceptance/pages/studio/utils.py
Normal file
35
common/test/acceptance/pages/studio/utils.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Utility methods useful for Studio page tests.
|
||||
"""
|
||||
from bok_choy.promise import Promise
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
|
||||
def click_css(page, css, source_index, require_notification=True):
|
||||
"""
|
||||
Click the button/link with the given css and index on the specified page (subclass of PageObject).
|
||||
|
||||
If require_notification is False (default value is True), the method will return immediately.
|
||||
Otherwise, it will wait for the "mini-notification" to appear and disappear.
|
||||
"""
|
||||
buttons = page.q(css=css)
|
||||
target = buttons[source_index]
|
||||
ActionChains(page.browser).click(target).release().perform()
|
||||
if require_notification:
|
||||
wait_for_notification(page)
|
||||
|
||||
|
||||
def wait_for_notification(page):
|
||||
"""
|
||||
Waits for the "mini-notification" to appear and disappear on the given page (subclass of PageObject).
|
||||
"""
|
||||
def _is_saving():
|
||||
num_notifications = len(page.q(css='.wrapper-notification-mini.is-shown'))
|
||||
return (num_notifications == 1, num_notifications)
|
||||
|
||||
def _is_saving_done():
|
||||
num_notifications = len(page.q(css='.wrapper-notification-mini.is-hiding'))
|
||||
return (num_notifications == 1, num_notifications)
|
||||
|
||||
Promise(_is_saving, 'Notification showing.').fulfill()
|
||||
Promise(_is_saving_done, 'Notification hidden.').fulfill()
|
||||
@@ -64,7 +64,7 @@ class RegistrationTest(UniqueCourseTest):
|
||||
course_names = dashboard.available_courses
|
||||
self.assertIn(self.course_info['display_name'], course_names)
|
||||
|
||||
@skip("TE-399")
|
||||
|
||||
class LanguageTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that the change language functionality on the dashboard works
|
||||
|
||||
@@ -38,6 +38,20 @@ class ContainerBase(UniqueCourseTest):
|
||||
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 = "Duplicate of '{0}'"
|
||||
self.discussion_label = "Discussion"
|
||||
|
||||
self.setup_fixtures()
|
||||
|
||||
self.auth_page.visit()
|
||||
@@ -79,13 +93,6 @@ class ContainerBase(UniqueCourseTest):
|
||||
container = unit.components[0].go_to_container()
|
||||
return container
|
||||
|
||||
|
||||
class DragAndDropTest(ContainerBase):
|
||||
"""
|
||||
Tests of reordering within the container page.
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
def verify_ordering(self, container, expected_orderings):
|
||||
xblocks = container.xblocks
|
||||
for expected_ordering in expected_orderings:
|
||||
@@ -101,25 +108,38 @@ class DragAndDropTest(ContainerBase):
|
||||
self.assertEqual(expected, children[idx].name)
|
||||
break
|
||||
|
||||
def drag_and_verify(self, source, target, expected_ordering, after=True):
|
||||
def do_action_and_verify(self, action, expected_ordering):
|
||||
container = self.go_to_container_page(make_draft=True)
|
||||
container.drag(source, target, after)
|
||||
action(container)
|
||||
|
||||
self.verify_ordering(container, expected_ordering)
|
||||
|
||||
# Reload the page to see that the reordering was saved persisted.
|
||||
# Reload the page to see that the change was persisted.
|
||||
container = self.go_to_container_page()
|
||||
self.verify_ordering(container, expected_ordering)
|
||||
|
||||
|
||||
class DragAndDropTest(ContainerBase):
|
||||
"""
|
||||
Tests of reordering within the container page.
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
def drag_and_verify(self, source, target, expected_ordering):
|
||||
self.do_action_and_verify(
|
||||
lambda (container): container.drag(source, target),
|
||||
expected_ordering
|
||||
)
|
||||
|
||||
def test_reorder_in_group(self):
|
||||
"""
|
||||
Drag Group B Item 2 before Group B Item 1.
|
||||
Drag Group A Item 2 before 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, self.group_a_item_2]},
|
||||
{self.group_b: [self.group_b_item_2, self.group_b_item_1]},
|
||||
{self.group_a: [self.group_a_item_2, self.group_a_item_1]},
|
||||
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
||||
{self.group_empty: []}]
|
||||
self.drag_and_verify(6, 4, expected_ordering)
|
||||
self.drag_and_verify(self.group_a_item_2_handle, self.group_a_item_1_handle, expected_ordering)
|
||||
|
||||
def test_drag_to_top(self):
|
||||
"""
|
||||
@@ -129,35 +149,157 @@ class DragAndDropTest(ContainerBase):
|
||||
{self.group_a: [self.group_a_item_2]},
|
||||
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
||||
{self.group_empty: []}]
|
||||
self.drag_and_verify(1, 0, expected_ordering, False)
|
||||
self.drag_and_verify(self.group_a_item_1_handle, self.group_a_handle, expected_ordering)
|
||||
|
||||
def test_drag_into_different_group(self):
|
||||
"""
|
||||
Drag Group A Item 1 into Group B (last element).
|
||||
Drag Group B Item 1 into Group A (first element).
|
||||
"""
|
||||
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_a_item_1]},
|
||||
{self.group_a: [self.group_b_item_1, self.group_a_item_1, self.group_a_item_2]},
|
||||
{self.group_b: [self.group_b_item_2]},
|
||||
{self.group_empty: []}]
|
||||
self.drag_and_verify(1, 6, expected_ordering)
|
||||
self.drag_and_verify(self.group_b_item_1_handle, self.group_a_item_1_handle, expected_ordering)
|
||||
|
||||
def test_drag_group_into_group(self):
|
||||
"""
|
||||
Drag Group B into Group A (last element).
|
||||
Drag Group B into Group A (first element).
|
||||
"""
|
||||
expected_ordering = [{self.container_title: [self.group_a, self.group_empty]},
|
||||
{self.group_a: [self.group_a_item_1, self.group_a_item_2, self.group_b]},
|
||||
{self.group_a: [self.group_b, 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.drag_and_verify(4, 2, expected_ordering)
|
||||
self.drag_and_verify(self.group_b_handle, self.group_a_item_1_handle, expected_ordering)
|
||||
|
||||
# Not able to drag into the empty group with automation (difficult even outside of automation).
|
||||
# def test_drag_into_empty(self):
|
||||
# """
|
||||
# Drag Group B Item 1 to Group Empty.
|
||||
# """
|
||||
# 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_2]},
|
||||
# {self.group_empty: [self.group_b_item_1]}]
|
||||
# self.drag_and_verify(6, 4, expected_ordering, False)
|
||||
def test_drag_after_addition(self):
|
||||
"""
|
||||
Add some components and then verify that drag and drop still works.
|
||||
"""
|
||||
group_a_menu = 0
|
||||
|
||||
def add_new_components_and_rearrange(container):
|
||||
# Add a video component to Group 1
|
||||
container.add_discussion(group_a_menu)
|
||||
# Duplicate the first item in Group A
|
||||
container.duplicate(self.group_a_item_1_action_index)
|
||||
|
||||
first_handle = self.group_a_item_1_handle
|
||||
# Drag newly added video component to top.
|
||||
container.drag(first_handle + 3, first_handle)
|
||||
# Drag duplicated component to top.
|
||||
container.drag(first_handle + 2, first_handle)
|
||||
|
||||
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: [duplicate_label, self.discussion_label, 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.do_action_and_verify(add_new_components_and_rearrange, expected_ordering)
|
||||
|
||||
|
||||
class AddComponentTest(ContainerBase):
|
||||
"""
|
||||
Tests of adding a component to the container page.
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
def add_and_verify(self, menu_index, expected_ordering):
|
||||
self.do_action_and_verify(
|
||||
lambda (container): container.add_discussion(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)
|
||||
|
||||
|
||||
class DuplicateComponentTest(ContainerBase):
|
||||
"""
|
||||
Tests of duplicating a component on the container page.
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class DeleteComponentTest(ContainerBase):
|
||||
"""
|
||||
Tests of deleting a component from the container page.
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
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: []}]
|
||||
self.delete_and_verify(self.group_a_item_1_action_index, expected_ordering)
|
||||
|
||||
@@ -577,9 +577,12 @@ class Html5VideoTest(VideoBaseTest):
|
||||
self.assertTrue(self.video.is_error_message_shown)
|
||||
|
||||
# Verify that error message has correct text
|
||||
correct_error_message_text = 'ERROR: No playable video sources found!'
|
||||
correct_error_message_text = 'No playable video sources found.'
|
||||
self.assertIn(correct_error_message_text, self.video.error_message_text)
|
||||
|
||||
# Verify that spinner is not shown
|
||||
self.assertFalse(self.video.is_spinner_shown())
|
||||
|
||||
def test_download_button_wo_english_transcript(self):
|
||||
"""
|
||||
Scenario: Download button works correctly w/o english transcript in HTML5 mode
|
||||
|
||||
Reference in New Issue
Block a user