diff --git a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py index 89b8cdefd86b..f79808a7ec9a 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py @@ -3,6 +3,7 @@ """ import json from gettext import GNUTranslations +from django.test import TestCase from completion.test_utils import CompletionWaffleTestMixin from django.db import connections, transaction @@ -24,6 +25,7 @@ from openedx.core.djangoapps.dark_lang.models import DarkLangConfig from openedx.core.djangoapps.xblock import api as xblock_api from openedx.core.djangolib.testing.utils import skip_unless_lms, skip_unless_cms +from openedx.core.lib.xblock_serializer import api as serializer_api from common.djangoapps.student.tests.factories import UserFactory @@ -59,6 +61,86 @@ def setUp(self): ) +@skip_unless_cms +class ContentLibraryOlxTests(ContentLibraryContentTestMixin, TestCase): + """ + Basic test of the Learning-Core-based XBlock serialization-deserialization, using XBlocks in a content library. + """ + + def test_html_round_trip(self): + """ + Test that if we deserialize and serialize an HTMLBlock repeatedly, two things hold true: + + 1. Even if the OLX changes format, the inner content does not change format. + 2. The OLX settles into a stable state after 1 round trip. + + (We are particularly testing HTML, but it would be good to confirm that these principles hold true for + XBlocks in general.) + """ + usage_key = library_api.create_library_block(self.library.key, "html", "roundtrip").usage_key + + # The block's actual HTML has some extraneous spaces and newlines, as well as comment. + # We expect this to be preserved through the round-trips. + block_content = '''\ +
+
+

There is a space on either side of this sentence.

+

\tThere is a tab on either side of this sentence.\t

+

🙃There is an emoji on either side of this sentence.🙂

+

There is nothing on either side of this sentence.

+
+

\t ]]>

+ +
''' + + # The OLX containing the HTML also has some extraneous stuff, which do *not* expect to survive the round-trip. + olx_1 = f'''\ + + ''' + + # Here is what we expect the OLX to settle down to. Notable changes: + # * url_name is added. + # * some_fake_field is gone. + # * The OLX comment is gone. + # * A trailing newline is added at the end of the export. + # DEVS: If you are purposefully tweaking the formatting of the xblock serializer, then it's fine to + # update the value of this variable, as long as: + # 1. the {block_content} remains unchanged, and + # 2. the canonical_olx remains stable through the 2nd round trip. + canonical_olx = ( + f'\n' + ) + + # Save the block to LC, and re-load it. + library_api.set_library_block_olx(usage_key, olx_1) + library_api.publish_changes(self.library.key) + block_saved_1 = xblock_api.load_block(usage_key, self.staff_user) + + # Content should be preserved... + assert block_saved_1.data == block_content + + # ...but the serialized OLX will have changed to match the 'canonical' OLX. + olx_2 = serializer_api.serialize_xblock_to_olx(block_saved_1).olx_str + assert olx_2 == canonical_olx + + # Now, save that OLX back to LC, and re-load it again. + library_api.set_library_block_olx(usage_key, olx_2) + library_api.publish_changes(self.library.key) + block_saved_2 = xblock_api.load_block(usage_key, self.staff_user) + + # Again, content should be preserved... + assert block_saved_2.data == block_saved_1.data == block_content + + # ...and this time, the OLX should have settled too. + olx_3 = serializer_api.serialize_xblock_to_olx(block_saved_2).olx_str + assert olx_3 == olx_2 == canonical_olx + + class ContentLibraryRuntimeTests(ContentLibraryContentTestMixin): """ Basic tests of the Learning-Core-based XBlock runtime using XBlocks in a diff --git a/openedx/core/djangoapps/content_staging/tests/test_clipboard.py b/openedx/core/djangoapps/content_staging/tests/test_clipboard.py index 00c4466b7d48..24925ac42edb 100644 --- a/openedx/core/djangoapps/content_staging/tests/test_clipboard.py +++ b/openedx/core/djangoapps/content_staging/tests/test_clipboard.py @@ -1,7 +1,6 @@ """ Tests for the clipboard functionality """ -from textwrap import dedent from xml.etree import ElementTree from rest_framework.test import APIClient @@ -155,11 +154,11 @@ def test_copy_html(self): assert olx_response.get("Content-Type") == "application/vnd.openedx.xblock.v1.html+xml" # For HTML, we really want to be sure that the OLX is serialized in this exact format (using CDATA), so we check # the actual string directly rather than using assertXmlEqual(): - assert olx_response.content.decode() == dedent(""" - Sample - ]]> - """).lstrip() + assert olx_response.content.decode() == ( + "Sample" + "]]>\n" + ) # Now if we GET the clipboard again, the GET response should exactly equal the last POST response: assert client.get(CLIPBOARD_ENDPOINT).json() == response_data diff --git a/openedx/core/lib/xblock_serializer/block_serializer.py b/openedx/core/lib/xblock_serializer/block_serializer.py index 966380f25061..f12bf5336af5 100644 --- a/openedx/core/lib/xblock_serializer/block_serializer.py +++ b/openedx/core/lib/xblock_serializer/block_serializer.py @@ -133,7 +133,7 @@ def _serialize_html_block(self, block) -> etree.Element: # Escape any CDATA special chars escaped_block_data = block.data.replace("]]>", "]]>") - olx_node.text = etree.CDATA("\n" + escaped_block_data + "\n") + olx_node.text = etree.CDATA(escaped_block_data) return olx_node