Commit d25673ec by E. Kolpakov

LibraryContent bok choy acceptance tests

parent e1f6ca93
......@@ -119,6 +119,7 @@ class LibraryContentFields(object):
scope=Scope.settings,
)
mode = String(
display_name=_("Mode"),
help=_("Determines how content is drawn from the library"),
default="random",
values=[
......
......@@ -375,5 +375,3 @@ class CourseFixture(XBlockContainerFixture):
"""
super(CourseFixture, self)._create_xblock_children(parent_loc, xblock_descriptions)
self._publish_xblock(parent_loc)
......@@ -27,6 +27,7 @@ class LibraryFixture(XBlockContainerFixture):
'display_name': display_name
}
self.display_name = display_name
self._library_key = None
super(LibraryFixture, self).__init__()
......
"""
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):
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 '{}[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)
......@@ -3,8 +3,12 @@ Library edit page in Studio
"""
from bok_choy.page_object import PageObject
from ...pages.studio.pagination import PaginatedMixin
from bok_choy.promise import EmptyPromise
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.select import Select
from .overview import CourseOutlineModal
from .container import XBlockWrapper
from ...pages.studio.pagination import PaginatedMixin
from ...tests.helpers import disable_animations
from .utils import confirm_prompt, wait_for_notification
from . import BASE_URL
......@@ -48,7 +52,10 @@ class LibraryPage(PageObject, PaginatedMixin):
for improved test reliability.
"""
self.wait_for_ajax()
self.wait_for_element_invisibility('.ui-loading', 'Wait for the page to complete its initial loading of XBlocks via AJAX')
self.wait_for_element_invisibility(
'.ui-loading',
'Wait for the page to complete its initial loading of XBlocks via AJAX'
)
disable_animations(self)
@property
......@@ -80,14 +87,18 @@ class LibraryPage(PageObject, PaginatedMixin):
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
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)
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):
"""
......@@ -95,4 +106,162 @@ class LibraryPage(PageObject, PaginatedMixin):
buttons.
action is 'edit', 'duplicate', or 'delete'
"""
return self._div_for_xblock_id(xblock_id)[0].find_element_by_css_selector('.header-actions .{action}-button.action-button'.format(action=action))
return self._div_for_xblock_id(xblock_id)[0].find_element_by_css_selector(
'.header-actions .{action}-button.action-button'.format(action=action)
)
class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
"""
Library Content XBlock Modal edit window
"""
url = None
MODAL_SELECTOR = ".wrapper-modal-window-edit-xblock"
# Labels used to identify the fields on the edit modal:
LIBRARY_LABEL = "Libraries"
COUNT_LABEL = "Count"
SCORED_LABEL = "Scored"
def is_browser_on_page(self):
"""
Check that we are on the right page in the browser.
"""
return self.is_shown()
@property
def library_key(self):
"""
Gets value of first library key input
"""
library_key_input = self.get_metadata_input(self.LIBRARY_LABEL)
if library_key_input is not None:
return library_key_input.get_attribute('value').strip(',')
return None
@library_key.setter
def library_key(self, library_key):
"""
Sets value of first library key input, creating it if necessary
"""
library_key_input = self.get_metadata_input(self.LIBRARY_LABEL)
if library_key_input is None:
library_key_input = self._add_library_key()
if library_key is not None:
# can't use lib_text.clear() here as input get deleted by client side script
library_key_input.send_keys(Keys.HOME)
library_key_input.send_keys(Keys.SHIFT, Keys.END)
library_key_input.send_keys(library_key)
else:
library_key_input.clear()
EmptyPromise(lambda: self.library_key == library_key, "library_key is updated in modal.").fulfill()
@property
def count(self):
"""
Gets value of children count input
"""
return int(self.get_metadata_input(self.COUNT_LABEL).get_attribute('value'))
@count.setter
def count(self, count):
"""
Sets value of children count input
"""
count_text = self.get_metadata_input(self.COUNT_LABEL)
count_text.clear()
count_text.send_keys(count)
EmptyPromise(lambda: self.count == count, "count is updated in modal.").fulfill()
@property
def scored(self):
"""
Gets value of scored select
"""
value = self.get_metadata_input(self.SCORED_LABEL).get_attribute('value')
if value == 'True':
return True
elif value == 'False':
return False
raise ValueError("Unknown value {value} set for {label}".format(value=value, label=self.SCORED_LABEL))
@scored.setter
def scored(self, scored):
"""
Sets value of scored select
"""
select_element = self.get_metadata_input(self.SCORED_LABEL)
select_element.click()
scored_select = Select(select_element)
scored_select.select_by_value(str(scored))
EmptyPromise(lambda: self.scored == scored, "scored is updated in modal.").fulfill()
def _add_library_key(self):
"""
Adds library key input
"""
wrapper = self._get_metadata_element(self.LIBRARY_LABEL)
add_button = wrapper.find_element_by_xpath(".//a[contains(@class, 'create-action')]")
add_button.click()
return self._get_list_inputs(wrapper)[0]
def _get_list_inputs(self, list_wrapper):
"""
Finds nested input elements (useful for List and Dict fields)
"""
return list_wrapper.find_elements_by_xpath(".//input[@type='text']")
def _get_metadata_element(self, metadata_key):
"""
Gets metadata input element (a wrapper div for List and Dict fields)
"""
metadata_inputs = self.find_css(".metadata_entry .wrapper-comp-setting label.setting-label")
target_label = [elem for elem in metadata_inputs if elem.text == metadata_key][0]
label_for = target_label.get_attribute('for')
return self.find_css("#" + label_for)[0]
def get_metadata_input(self, metadata_key):
"""
Gets input/select element for given field
"""
element = self._get_metadata_element(metadata_key)
if element.tag_name == 'div':
# List or Dict field - return first input
# TODO support multiple values
inputs = self._get_list_inputs(element)
element = inputs[0] if inputs else None
return element
class StudioLibraryContainerXBlockWrapper(XBlockWrapper):
"""
Wraps :class:`.container.XBlockWrapper` for use with LibraryContent blocks
"""
url = None
@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)
@property
def header_text(self):
"""
Gets library content text
"""
return self.get_body_paragraphs().first.text[0]
def get_body_paragraphs(self):
"""
Gets library content body paragraphs
"""
return self.q(css=self._bounded_selector(".xblock-message-area p"))
def refresh_children(self):
"""
Click "Update now..." button
"""
refresh_button = self.q(css=self._bounded_selector(".library-update-btn"))
refresh_button.click()
# -*- coding: utf-8 -*-
"""
End-to-end tests for LibraryContent block in LMS
"""
import ddt
from ..helpers import UniqueCourseTest
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.overview import CourseOutlinePage
from ...pages.studio.library import StudioLibraryContentXBlockEditModal, StudioLibraryContainerXBlockWrapper
from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.library import LibraryContentXBlockWrapper
from ...pages.common.logout import LogoutPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ...fixtures.library import LibraryFixture
SECTION_NAME = 'Test Section'
SUBSECTION_NAME = 'Test Subsection'
UNIT_NAME = 'Test Unit'
@ddt.ddt
class LibraryContentTest(UniqueCourseTest):
"""
Test courseware.
"""
USERNAME = "STUDENT_TESTER"
EMAIL = "student101@example.com"
STAFF_USERNAME = "STAFF_TESTER"
STAFF_EMAIL = "staff101@example.com"
def setUp(self):
"""
Set up library, course and library content XBlock
"""
super(LibraryContentTest, self).setUp()
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
self.library_fixture = LibraryFixture('test_org', self.unique_id, 'Test Library {}'.format(self.unique_id))
self.library_fixture.add_children(
XBlockFixtureDesc("html", "Html1", data='html1'),
XBlockFixtureDesc("html", "Html2", data='html2'),
XBlockFixtureDesc("html", "Html3", data='html3'),
)
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_libraries': [self.library_key],
'mode': 'random',
'max_count': 1,
'has_score': False
}
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 _refresh_library_content_children(self, count=1):
"""
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[0])
modal = StudioLibraryContentXBlockEditModal(library_container_block.edit())
modal.count = count
library_container_block.save_settings()
library_container_block.refresh_children()
self._go_to_unit_page(change_login=False)
unit_page.wait_for_page()
unit_page.publish_action.click()
unit_page.wait_for_ajax()
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.course_outline.visit()
subsection = self.course_outline.section(SECTION_NAME).subsection(SUBSECTION_NAME)
return subsection.toggle_expand().unit(UNIT_NAME).go_to()
def _goto_library_block_page(self, block_id=None):
"""
Open library page in LMS
"""
self.courseware_page.visit()
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)
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.data(1, 2, 3)
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._refresh_library_content_children(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._refresh_library_content_children(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), 3)
self.assertEqual(children_contents, self.library_xblocks_texts)
......@@ -109,8 +109,9 @@ class StudioLibraryTest(WebAppTest):
"""
Base class for all Studio library tests.
"""
as_staff = True
def setUp(self, is_staff=False): # pylint: disable=arguments-differ
def setUp(self): # pylint: disable=arguments-differ
"""
Install a library with no content using a fixture.
"""
......@@ -122,10 +123,11 @@ class StudioLibraryTest(WebAppTest):
)
self.populate_library_fixture(fixture)
fixture.install()
self.library_fixture = fixture
self.library_info = fixture.library_info
self.library_key = fixture.library_key
self.user = fixture.user
self.log_in(self.user, is_staff)
self.log_in(self.user, self.as_staff)
def populate_library_fixture(self, library_fixture):
"""
......
......@@ -18,7 +18,7 @@ class LibraryEditPageTest(StudioLibraryTest):
"""
Ensure a library exists and navigate to the library edit page.
"""
super(LibraryEditPageTest, self).setUp(is_staff=True)
super(LibraryEditPageTest, self).setUp()
self.lib_page = LibraryPage(self.browser, self.library_key)
self.lib_page.visit()
self.lib_page.wait_until_ready()
......@@ -156,7 +156,7 @@ class LibraryNavigationTest(StudioLibraryTest):
"""
Ensure a library exists and navigate to the library edit page.
"""
super(LibraryNavigationTest, self).setUp(is_staff=True)
super(LibraryNavigationTest, self).setUp()
self.lib_page = LibraryPage(self.browser, self.library_key)
self.lib_page.visit()
self.lib_page.wait_until_ready()
......
"""
Acceptance tests for Library Content in LMS
"""
import ddt
from .base_studio_test import StudioLibraryTest, ContainerBase
from ...pages.studio.library import StudioLibraryContentXBlockEditModal, StudioLibraryContainerXBlockWrapper
from ...fixtures.course import XBlockFixtureDesc
SECTION_NAME = 'Test Section'
SUBSECTION_NAME = 'Test Subsection'
UNIT_NAME = 'Test Unit'
@ddt.ddt
class StudioLibraryContainerTest(ContainerBase, StudioLibraryTest):
"""
Test Library Content block in LMS
"""
def setUp(self):
"""
Install library with some content and a course using fixtures
"""
super(StudioLibraryContainerTest, self).setUp()
self.outline.visit()
subsection = self.outline.section(SECTION_NAME).subsection(SUBSECTION_NAME)
self.unit_page = subsection.toggle_expand().unit(UNIT_NAME).go_to()
def populate_library_fixture(self, library_fixture):
"""
Populate the children of the test course fixture.
"""
library_fixture.add_children(
XBlockFixtureDesc("html", "Html1"),
XBlockFixtureDesc("html", "Html2"),
XBlockFixtureDesc("html", "Html3"),
)
def populate_course_fixture(self, course_fixture):
""" Install a course with sections/problems, tabs, updates, and handouts """
library_content_metadata = {
'source_libraries': [self.library_key],
'mode': 'random',
'max_count': 1,
'has_score': False
}
course_fixture.add_children(
XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children(
XBlockFixtureDesc('vertical', UNIT_NAME).add_children(
XBlockFixtureDesc('library_content', "Library Content", metadata=library_content_metadata)
)
)
)
)
def _get_library_xblock_wrapper(self, xblock):
"""
Wraps xblock into :class:`...pages.studio.library.StudioLibraryContainerXBlockWrapper`
"""
return StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(xblock)
@ddt.data(
('library-v1:111+111', 1, True),
('library-v1:edX+L104', 2, False),
('library-v1:OtherX+IDDQD', 3, True),
)
@ddt.unpack
def test_can_edit_metadata(self, library_key, max_count, scored):
"""
Scenario: Given I have a library, a course and library content xblock in a course
When I go to studio unit page for library content block
And I edit library content metadata and save it
Then I can ensure that data is persisted
"""
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
edit_modal.library_key = library_key
edit_modal.count = max_count
edit_modal.scored = scored
library_container.save_settings() # saving settings
# open edit window again to verify changes are persistent
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
self.assertEqual(edit_modal.library_key, library_key)
self.assertEqual(edit_modal.count, max_count)
self.assertEqual(edit_modal.scored, scored)
def test_no_library_shows_library_not_configured(self):
"""
Scenario: Given I have a library, a course and library content xblock in a course
When I go to studio unit page for library content block
And I edit set library key to none
Then I can see that library content block is misconfigured
"""
expected_text = 'No library or filters configured. Press "Edit" to configure.'
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
# precondition check - assert library is configured before we remove it
self.assertNotIn(expected_text, library_container.header_text)
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
edit_modal.library_key = None
library_container.save_settings()
self.assertIn(expected_text, library_container.header_text)
@ddt.data(
'library-v1:111+111',
'library-v1:edX+L104',
)
def test_set_missing_library_shows_correct_label(self, library_key):
"""
Scenario: Given I have a library, a course and library content xblock in a course
When I go to studio unit page for library content block
And I edit set library key to non-existent library
Then I can see that library content block is misconfigured
"""
expected_text = "Library is invalid, corrupt, or has been deleted."
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
# precondition check - assert library is configured before we remove it
self.assertNotIn(expected_text, library_container.header_text)
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
edit_modal.library_key = library_key
library_container.save_settings()
self.assertIn(expected_text, library_container.header_text)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment