diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 663b637e51..850071e533 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -206,6 +206,18 @@ class ContainerPage(PageObject, HelpMixin): """ return self.q(css='.wrapper-xblock .level-element .header-details').text + @property + def content_html(self): + """ + Gets the html of HTML module + Returns: + list: A list containing inner HTMl + """ + self.wait_for_element_visibility('.xmodule_HtmlModule', 'Xblock content is visible') + html = self.q(css='.xmodule_HtmlModule').html + html = html[0].strip() + return html + @property def is_staff_locked(self): """ Returns True if staff lock is currently enabled, False otherwise """ diff --git a/common/test/acceptance/pages/studio/html_component_editor.py b/common/test/acceptance/pages/studio/html_component_editor.py index 50f2a83035..04cab54306 100644 --- a/common/test/acceptance/pages/studio/html_component_editor.py +++ b/common/test/acceptance/pages/studio/html_component_editor.py @@ -1,5 +1,8 @@ -from common.test.acceptance.pages.studio.utils import type_in_codemirror -from xblock_editor import XBlockEditorView +""" +HTML component editor in studio +""" +from common.test.acceptance.pages.studio.utils import type_in_codemirror, get_codemirror_value +from common.test.acceptance.pages.studio.xblock_editor import XBlockEditorView from common.test.acceptance.pages.common.utils import click_css @@ -12,6 +15,62 @@ class HtmlXBlockEditorView(XBlockEditorView): 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(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. @@ -74,6 +133,13 @@ class HtmlXBlockEditorView(XBlockEditorView): """ 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 @@ -95,6 +161,13 @@ class HtmlXBlockEditorView(XBlockEditorView): """ 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 @@ -109,6 +182,23 @@ class HtmlXBlockEditorView(XBlockEditorView): 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 @@ -138,6 +228,12 @@ class HtmlXBlockEditorView(XBlockEditorView): """ click_css(self, '.save-button') + def save_content(self): + """ + Click save button + """ + click_css(self, '.action-save') + class HTMLEditorIframe(XBlockEditorView): """ diff --git a/common/test/acceptance/tests/studio/test_studio_html_editor.py b/common/test/acceptance/tests/studio/test_studio_html_editor.py index 7cf2058454..ec95b668c2 100644 --- a/common/test/acceptance/tests/studio/test_studio_html_editor.py +++ b/common/test/acceptance/tests/studio/test_studio_html_editor.py @@ -1,6 +1,7 @@ """ Acceptance tests for HTML component in studio """ +from common.test.acceptance.pages.studio.utils import type_in_codemirror from common.test.acceptance.tests.studio.base_studio_test import ContainerBase from common.test.acceptance.fixtures.course import XBlockFixtureDesc from common.test.acceptance.pages.studio.container import ContainerPage, XBlockWrapper @@ -8,7 +9,7 @@ from common.test.acceptance.pages.studio.utils import add_component from common.test.acceptance.pages.studio.html_component_editor import HtmlXBlockEditorView, HTMLEditorIframe -class HTMLComponentEditor(ContainerBase): +class HTMLComponentEditorTests(ContainerBase): """ Feature: CMS.Component Adding As a course author, I want to be able to add and edit HTML component @@ -17,17 +18,13 @@ class HTMLComponentEditor(ContainerBase): """ Create a course with a section, subsection, and unit to which to add the component. """ - super(HTMLComponentEditor, self).setUp(is_staff=is_staff) - self.component = 'Text' + super(HTMLComponentEditorTests, self).setUp(is_staff=is_staff) self.unit = self.go_to_unit_page() self.container_page = ContainerPage(self.browser, None) self.xblock_wrapper = XBlockWrapper(self.browser, None) - # Add HTML component - add_component(self.container_page, 'html', self.component) - self.component = self.unit.xblocks[1] - self.container_page.edit() - self.html_editor = HtmlXBlockEditorView(self.browser, self.component.locator) - self.iframe = HTMLEditorIframe(self.browser, self.component.locator) + self.component = None + self.html_editor = None + self.iframe = None def populate_course_fixture(self, course_fixture): """ @@ -41,6 +38,29 @@ class HTMLComponentEditor(ContainerBase): ) ) + def _add_content(self, content): + """ + Set and save content in editor and assert its presence in container page's html + + Args: + content(str): Verifiable content + """ + self.html_editor.set_raw_content(content) + self.html_editor.save_content() + self.container_page.wait_for_page() + + def _add_component(self, sub_type): + """ + Add sub-type of HTML component in studio + + Args: + sub_type(str): Sub-type of HTML component + """ + add_component(self.container_page, 'html', sub_type) + self.component = self.unit.xblocks[1] + self.html_editor = HtmlXBlockEditorView(self.browser, self.component.locator) + self.iframe = HTMLEditorIframe(self.browser, self.component.locator) + def test_user_can_view_metadata(self): """ Scenario: User can view metadata @@ -48,6 +68,10 @@ class HTMLComponentEditor(ContainerBase): And I edit and select Settings Then I see the HTML component settings """ + + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() self.html_editor.open_settings_tab() display_name_value = self.html_editor.get_default_settings()[0] display_name_key = self.html_editor.keys[0] @@ -72,6 +96,9 @@ class HTMLComponentEditor(ContainerBase): Then I can modify the display name And my display name change is persisted on save """ + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() self.html_editor.open_settings_tab() self.html_editor.set_field_val('Display Name', 'New Name') self.html_editor.save_settings() @@ -88,6 +115,10 @@ class HTMLComponentEditor(ContainerBase): And the link is shown as "/static/image.jpg" in the Link Plugin """ static_link = '/static/image.jpg' + + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() self.html_editor.open_link_plugin() self.html_editor.save_static_link(static_link) self.html_editor.switch_to_iframe() @@ -100,3 +131,234 @@ class HTMLComponentEditor(ContainerBase): static_link, "URL in the link plugin is different" ) + + def test_tinymce_and_codemirror_preserve_style_tags(self): + """ + Scenario: TinyMCE and CodeMirror preserve style tags + Given I have created a Blank HTML Page + When I edit the page + And type "

pages

" in the code editor and + press OK + And I save the page + Then the page text contains: + "" +

pages

+ + "" + """ + content = '

pages

' + + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() + self._add_content(content) + html = self.container_page.content_html + self.assertIn(content, html) + + def test_tinymce_and_codemirror_preserve_span_tags(self): + """ + Scenario: TinyMCE and CodeMirror preserve span tags + Given I have created a Blank HTML Page + When I edit the page + And type "Test" in the code editor and press OK + And I save the page + Then the page text contains: + "" + Test + "" + """ + content = "Test" + + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() + self._add_content(content) + html = self.container_page.content_html + self.assertIn(content, html) + + def test_tinymce_and_codemirror_preserve_math_tags(self): + """ + Scenario: TinyMCE and CodeMirror preserve math tags + Given I have created a Blank HTML Page + When I edit the page + And type "x2" in the code editor and press OK + And I save the page + Then the page text contains: + "" + x2 + "" + """ + content = "x2" + + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() + self._add_content(content) + html = self.container_page.content_html + self.assertIn(content, html) + + def test_code_format_toolbar_wraps_text_with_code_tags(self): + """ + Scenario: Code format toolbar button wraps text with code tags + Given I have created a Blank HTML Page + When I edit the page + And I set the text to "display as code" and I select the text + And I save the page + Then the page text contains: + "" +

display as code

+ "" + """ + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() + self.html_editor.set_text_and_select("display as code") + self.html_editor.click_code_toolbar_button() + self.html_editor.save_content() + html = self.container_page.content_html + self.assertIn(html, '

display as code

') + + def test_raw_html_component_does_not_change_text(self): + """ + Scenario: Raw HTML component does not change text + Given I have created a raw HTML component + When I edit the page + And type "
  • zzzz
      " into the Raw Editor + And I save the page + Then the page text contains: + "" +
    1. zzzz
        + "" + And I edit the page + Then the Raw Editor contains exactly: + "" +
      1. zzzz
          + "" + """ + content = "
        1. zzzz
        2. " + + # Add Raw HTML type component + self._add_component('Raw HTML') + self.container_page.edit() + + # Set content in tinymce editor + type_in_codemirror(self.html_editor, 0, content) + self.html_editor.save_content() + + # The HTML of the content added through tinymce editor + html = self.container_page.content_html + # The text content should be present with its tag preserved + self.assertIn(content, html) + + self.container_page.edit() + editor_value = self.html_editor.editor_value + # The tinymce editor value should not be different from the content added in the start + self.assertEqual(content, editor_value) + + def test_tinymce_toolbar_buttons_are_as_expected(self): + """ + Scenario: TinyMCE toolbar buttons are as expected + Given I have created a Blank HTML Page + When I edit the page + Then the expected toolbar buttons are displayed + """ + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() + + expected_buttons = [ + u'bold', + u'italic', + u'underline', + u'forecolor', + # This is our custom "code style" button, which uses an image instead of a class. + u'none', + u'alignleft', + u'aligncenter', + u'alignright', + u'alignjustify', + u'bullist', + u'numlist', + u'outdent', + u'indent', + u'blockquote', + u'link', + u'unlink', + u'image' + ] + toolbar_dropdowns = self.html_editor.toolbar_dropdown_titles + # The toolbar is divided in two sections: drop-downs and all other formatting buttons + # The assertions under asserts for the drop-downs + self.assertEqual(len(toolbar_dropdowns), 2) + self.assertEqual(['Paragraph', 'Font Family'], toolbar_dropdowns) + + toolbar_buttons = self.html_editor.toolbar_button_titles + # The assertions under asserts for all the remaining formatting buttons + self.assertEqual(len(toolbar_buttons), len(expected_buttons)) + + for index, button in enumerate(expected_buttons): + class_name = toolbar_buttons[index] + self.assertEqual("mce-ico mce-i-" + button, class_name) + + def test_static_links_converted(self): + """ + Scenario: Static links are converted when switching between code editor and WYSIWYG views + Given I have created a Blank HTML Page + When I edit the page + And type "" in the code editor and press OK + Then the src link is rewritten to the asset link /asset-v1:(course_id)+type@asset+block/image.jpg + And the code editor displays "

          " + """ + value = '' + + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() + self.html_editor.set_raw_content(value) + self.html_editor.save_content() + html = self.container_page.content_html + src = "/asset-v1:{}+type@asset+block/image.jpg".format(self.course_id.strip('course-v1:')) + self.assertIn(src, html) + self.container_page.edit() + self.html_editor.open_raw_editor() + editor_value = self.html_editor.editor_value + self.assertEqual(value, editor_value) + + def test_font_selection_dropdown(self): + """ + Scenario: Font selection dropdown contains Default font and tinyMCE builtin fonts + Given I have created a Blank HTML Page + When I edit the page + And I click font selection dropdown + Then I should see a list of available fonts + And "Default" fonts should be available + And all standard tinyMCE fonts should be available + """ + # Add HTML Text type component + self._add_component('Text') + self.container_page.edit() + EXPECTED_FONTS = { + u"Default": [u'"Open Sans"', u'Verdana', u'Arial', u'Helvetica', u'sans-serif'], + u"Andale Mono": [u'andale mono', u'times'], + u"Arial": [u'arial', u'helvetica', u'sans-serif'], + u"Arial Black": [u'arial black', u'avant garde'], + u"Book Antiqua": [u'book antiqua', u'palatino'], + u"Comic Sans MS": [u'comic sans ms', u'sans-serif'], + u"Courier New": [u'courier new', u'courier'], + u"Georgia": [u'georgia', u'palatino'], + u"Helvetica": [u'helvetica'], + u"Impact": [u'impact', u'chicago'], + u"Symbol": [u'symbol'], + u"Tahoma": [u'tahoma', u'arial', u'helvetica', u'sans-serif'], + u"Terminal": [u'terminal', u'monaco'], + u"Times New Roman": [u'times new roman', u'times'], + u"Trebuchet MS": [u'trebuchet ms', u'geneva'], + u"Verdana": [u'verdana', u'geneva'], + # tinyMCE does not set font-family on dropdown span for these two fonts + u"Webdings": [u""], # webdings + u"Wingdings": [u""] # wingdings + } + self.html_editor.open_font_dropdown() + self.assertDictContainsSubset(EXPECTED_FONTS, self.html_editor.font_dict())