Allow editing of container xblocks/xmodules
STUD-1312
This commit is contained in:
66
common/test/acceptance/pages/studio/component_editor.py
Normal file
66
common/test/acceptance/pages/studio/component_editor.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from bok_choy.page_object import PageObject
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from utils import click_css
|
||||
|
||||
|
||||
class ComponentEditorView(PageObject):
|
||||
"""
|
||||
A :class:`.PageObject` representing the rendered view of a component editor.
|
||||
|
||||
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(ComponentEditorView, 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 `ComponentEditorView` context
|
||||
"""
|
||||
return '{}[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 get_setting_entry_index(self, label):
|
||||
"""
|
||||
Returns the index of the setting entry with given label (display name) within the Settings modal.
|
||||
"""
|
||||
# TODO: will need to handle tabbed "Settings" in future (current usage is in vertical, only shows Settings.
|
||||
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 index
|
||||
return None
|
||||
|
||||
def set_field_value_and_save(self, label, value):
|
||||
"""
|
||||
Set the field with given label (display name) to the specified value, and presses Save.
|
||||
"""
|
||||
index = self.get_setting_entry_index(label)
|
||||
elem = self.q(css=self._bounded_selector('.metadata_edit div.wrapper-comp-setting input.setting-input'))[index]
|
||||
# Click in the field, delete the value there.
|
||||
action = ActionChains(self.browser).click(elem)
|
||||
for _x in range(0, len(elem.get_attribute('value'))):
|
||||
action = action.send_keys(Keys.BACKSPACE)
|
||||
# Send the new text, then Tab to move to the next field (so change event is triggered).
|
||||
action.send_keys(value).send_keys(Keys.TAB).perform()
|
||||
click_css(self, 'a.action-save')
|
||||
@@ -3,7 +3,7 @@ Container page in Studio
|
||||
"""
|
||||
|
||||
from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import Promise
|
||||
from bok_choy.promise import Promise, EmptyPromise
|
||||
from . import BASE_URL
|
||||
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
@@ -15,15 +15,24 @@ class ContainerPage(PageObject):
|
||||
"""
|
||||
Container page in Studio
|
||||
"""
|
||||
NAME_SELECTOR = 'a.navigation-current'
|
||||
|
||||
def __init__(self, browser, unit_locator):
|
||||
def __init__(self, browser, locator):
|
||||
super(ContainerPage, self).__init__(browser)
|
||||
self.unit_locator = unit_locator
|
||||
self.locator = locator
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""URL to the container page for an xblock."""
|
||||
return "{}/container/{}".format(BASE_URL, self.unit_locator)
|
||||
return "{}/container/{}".format(BASE_URL, self.locator)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
titles = self.q(css=self.NAME_SELECTOR).text
|
||||
if titles:
|
||||
return titles[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def is_browser_on_page(self):
|
||||
|
||||
@@ -91,6 +100,14 @@ class ContainerPage(PageObject):
|
||||
# Click the confirmation dialog button
|
||||
click_css(self, 'a.button.action-primary', 0)
|
||||
|
||||
def edit(self):
|
||||
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()
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class XBlockWrapper(PageObject):
|
||||
|
||||
@@ -5,7 +5,7 @@ from bok_choy.promise import Promise
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
|
||||
def click_css(page, css, source_index, require_notification=True):
|
||||
def click_css(page, css, source_index=0, require_notification=True):
|
||||
"""
|
||||
Click the button/link with the given css and index on the specified page (subclass of PageObject).
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"""
|
||||
Acceptance tests for Studio related to the container page.
|
||||
"""
|
||||
|
||||
from ..pages.studio.auto_auth import AutoAuthPage
|
||||
from ..pages.studio.overview import CourseOutlinePage
|
||||
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
|
||||
from .helpers import UniqueCourseTest
|
||||
from ..pages.studio.component_editor import ComponentEditorView
|
||||
|
||||
from unittest import skip
|
||||
|
||||
|
||||
class ContainerBase(UniqueCourseTest):
|
||||
@@ -85,13 +89,17 @@ class ContainerBase(UniqueCourseTest):
|
||||
).install()
|
||||
|
||||
def go_to_container_page(self, make_draft=False):
|
||||
unit = self.go_to_unit_page(make_draft)
|
||||
container = unit.components[0].go_to_container()
|
||||
return container
|
||||
|
||||
def go_to_unit_page(self, make_draft=False):
|
||||
self.outline.visit()
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
if make_draft:
|
||||
unit.edit_draft()
|
||||
container = unit.components[0].go_to_container()
|
||||
return container
|
||||
return unit
|
||||
|
||||
def verify_ordering(self, container, expected_orderings):
|
||||
xblocks = container.xblocks
|
||||
@@ -131,6 +139,7 @@ class DragAndDropTest(ContainerBase):
|
||||
expected_ordering
|
||||
)
|
||||
|
||||
@skip("Sporadically drags outside of the Group.")
|
||||
def test_reorder_in_group(self):
|
||||
"""
|
||||
Drag Group A Item 2 before Group A Item 1.
|
||||
@@ -303,3 +312,36 @@ class DeleteComponentTest(ContainerBase):
|
||||
{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)
|
||||
|
||||
|
||||
class EditContainerTest(ContainerBase):
|
||||
"""
|
||||
Tests of editing a container.
|
||||
"""
|
||||
__test__ = True
|
||||
|
||||
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 = ComponentEditorView(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(make_draft=True)
|
||||
component = unit.components[0]
|
||||
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_container_page(make_draft=True)
|
||||
self.modify_display_name_and_verify(container)
|
||||
|
||||
Reference in New Issue
Block a user