Files
edx-platform/xmodule/tests/test_word_cloud.py
Muhammad Farhan Khan 4a9fc77ecb test: Allow for Extracted Word Cloud Block | Fix Test Cases (#35983)
Run tests for both the built-in and extracted WordCloud block.
The tests are mostly compatible with both versions of the block,
except for a few places where the XBlock framework and the
built-in XModule system differ which we've had to handle using
conditionals.

This moves us closer to enabling the extracted WordCloud block
by default and eventually removing the built-in block.

Part of: https://github.com/openedx/edx-platform/issues/34840
2025-08-08 11:22:26 -04:00

210 lines
8.7 KiB
Python

"""Test for Word Cloud Block functional logic."""
import json
import os
from unittest.mock import Mock
from django.conf import settings
from django.test import TestCase
from django.test import override_settings
from fs.memoryfs import MemoryFS
from lxml import etree
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from webob import Request
from webob.multidict import MultiDict
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule import word_cloud_block
from . import get_test_descriptor_system, get_test_system
class _TestWordCloudBase(TestCase):
"""
Logic tests for Word Cloud Block.
"""
__test__ = False
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.word_cloud_class = word_cloud_block.reset_class()
def setUp(self):
super().setUp()
self.raw_field_data = {
'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2},
'top_words': {'cat': 10, 'dog': 5, 'dad': 2},
'submitted': False,
'display_name': 'Word Cloud Block',
'instructions': 'Enter some random words that comes to your mind'
}
def test_xml_import_export_cycle(self):
"""
Test the import export cycle.
"""
runtime = get_test_descriptor_system()
runtime.export_fs = MemoryFS()
original_xml = (
'<word_cloud display_name="Favorite Fruits" display_student_percents="false" '
'instructions="What are your favorite fruits?" num_inputs="3" num_top_words="100"/>\n'
)
olx_element = etree.fromstring(original_xml)
runtime.id_generator = Mock()
def_id = runtime.id_generator.create_definition(olx_element.tag, olx_element.get('url_name'))
keys = ScopeIds(None, olx_element.tag, def_id, runtime.id_generator.create_usage(def_id))
block = self.word_cloud_class.parse_xml(olx_element, runtime, keys)
block.location = BlockUsageLocator(
CourseLocator('org', 'course', 'run', branch='revision'), 'word_cloud', 'block_id'
)
assert block.display_name == 'Favorite Fruits'
assert not block.display_student_percents
assert block.instructions == 'What are your favorite fruits?'
assert block.num_inputs == 3
assert block.num_top_words == 100
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# For extracted XBlocks, we need to manually export the XML definition to a file to properly test the
# import/export cycle. This is because extracted XBlocks use XBlock core's `add_xml_to_node` method,
# which does not export the XML to a file like `XmlMixin.add_xml_to_node` does.
filepath = 'word_cloud/block_id.xml'
runtime.export_fs.makedirs(os.path.dirname(filepath), recreate=True)
with runtime.export_fs.open(filepath, 'wb') as fileObj:
runtime.export_to_xml(block, fileObj)
else:
node = etree.Element("unknown_root")
# This will export the olx to a separate file.
block.add_xml_to_node(node)
with runtime.export_fs.open('word_cloud/block_id.xml') as f:
exported_xml = f.read()
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# For extracted XBlocks, we need to remove the `xblock-family` attribute from the exported XML to ensure
# consistency with the original XML.
# This is because extracted XBlocks use the core XBlock's `add_xml_to_node` method, which includes this
# attribute, whereas `XmlMixin.add_xml_to_node` does not.
exported_xml_tree = etree.fromstring(exported_xml.encode('utf-8'))
etree.cleanup_namespaces(exported_xml_tree)
if 'xblock-family' in exported_xml_tree.attrib:
del exported_xml_tree.attrib['xblock-family']
exported_xml = etree.tostring(exported_xml_tree, encoding='unicode', pretty_print=True)
assert exported_xml == original_xml
def test_bad_ajax_request(self):
"""
Make sure that answer for incorrect request is error json.
"""
module_system = get_test_system()
block = self.word_cloud_class(module_system, DictFieldData(self.raw_field_data), Mock())
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# The extracted Word Cloud XBlock uses @XBlock.json_handler for handling AJAX requests,
# which requires a different way of method invocation.
with self.assertRaises(AttributeError) as context:
json.loads(block.bad_dispatch('bad_dispatch', {}))
self.assertIn("'WordCloudBlock' object has no attribute 'bad_dispatch'", str(context.exception))
else:
response = json.loads(block.handle_ajax('bad_dispatch', {}))
self.assertDictEqual(response, {
'status': 'fail',
'error': 'Unknown Command!'
})
def test_good_ajax_request(self):
"""
Make sure that ajax request works correctly.
"""
module_system = get_test_system()
block = self.word_cloud_class(module_system, DictFieldData(self.raw_field_data), Mock())
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# The extracted Word Cloud XBlock uses @XBlock.json_handler for handling AJAX requests.
# It expects a standard Python dictionary as POST data and returns a JSON object in response.
post_data = {'student_words': ['cat', 'cat', 'dog', 'sun']}
response = block.submit_state(post_data)
else:
post_data = MultiDict(('student_words[]', word) for word in ['cat', 'cat', 'dog', 'sun'])
response = json.loads(block.handle_ajax('submit', post_data))
assert response['status'] == 'success'
assert response['submitted'] is True
assert response['total_count'] == 22
self.assertDictEqual(
response['student_words'],
{'sun': 1, 'dog': 6, 'cat': 12}
)
self.assertListEqual(
response['top_words'],
[{'text': 'cat', 'size': 12, 'percent': 55.0},
{'text': 'dad', 'size': 2, 'percent': 9.0},
{'text': 'dog', 'size': 6, 'percent': 27.0},
{'text': 'mom', 'size': 1, 'percent': 5.0},
{'text': 'sun', 'size': 1, 'percent': 4.0}]
)
assert 100.0 == sum(i['percent'] for i in response['top_words'])
def test_indexibility(self):
"""
Test indexibility of Word Cloud
"""
module_system = get_test_system()
block = self.word_cloud_class(module_system, DictFieldData(self.raw_field_data), Mock())
assert block.index_dictionary() ==\
{'content_type': 'Word Cloud',
'content': {'display_name': 'Word Cloud Block',
'instructions': 'Enter some random words that comes to your mind'}}
def test_studio_submit_handler(self):
"""
Test studio_submint handler
"""
TEST_SUBMIT_DATA = {
'display_name': "New Word Cloud",
'instructions': "This is a Test",
'num_inputs': 5,
'num_top_words': 10,
'display_student_percents': 'False',
}
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# In the extracted Word Cloud XBlock, we use StudioEditableXBlockMixin.submit_studio_edits,
# which expects a different handler name and request JSON format.
handler_name = 'submit_studio_edits'
TEST_REQUEST_JSON = {
'values': TEST_SUBMIT_DATA,
}
else:
handler_name = 'studio_submit'
TEST_REQUEST_JSON = TEST_SUBMIT_DATA
module_system = get_test_system()
block = self.word_cloud_class(module_system, DictFieldData(self.raw_field_data), Mock())
body = json.dumps(TEST_REQUEST_JSON)
request = Request.blank('/')
request.method = 'POST'
request.body = body.encode('utf-8')
res = block.handle(handler_name, request)
assert json.loads(res.body.decode('utf8')) == {'result': 'success'}
assert block.display_name == TEST_SUBMIT_DATA['display_name']
assert block.instructions == TEST_SUBMIT_DATA['instructions']
assert block.num_inputs == TEST_SUBMIT_DATA['num_inputs']
assert block.num_top_words == TEST_SUBMIT_DATA['num_top_words']
assert block.display_student_percents == (TEST_SUBMIT_DATA['display_student_percents'] == "True")
@override_settings(USE_EXTRACTED_WORD_CLOUD_BLOCK=True)
class TestWordCloudExtracted(_TestWordCloudBase):
__test__ = True
@override_settings(USE_EXTRACTED_WORD_CLOUD_BLOCK=False)
class TestWordCloudBuiltIn(_TestWordCloudBase):
__test__ = True