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
This commit is contained in:
Muhammad Farhan Khan
2025-08-08 20:22:26 +05:00
committed by GitHub
parent 17f0e256bd
commit 4a9fc77ecb
4 changed files with 202 additions and 58 deletions

View File

@@ -138,11 +138,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
self.setup_course()
self.initialize_module(metadata=self.METADATA, data=self.DATA)
def get_url(self, dispatch):
def get_url(self, dispatch, handler_name='xmodule_handler'):
"""Return item url with dispatch."""
return reverse(
'xblock_handler',
args=(str(self.course.id), quote_slashes(self.item_url), 'xmodule_handler', dispatch)
args=(str(self.course.id), quote_slashes(self.item_url), handler_name, dispatch)
)

View File

@@ -1,23 +1,40 @@
"""Word cloud integration tests using mongo modulestore."""
import importlib
import json
import re
from operator import itemgetter
from unittest.mock import patch
from uuid import UUID
import pytest
from django.conf import settings
from django.test import override_settings
from xblock import plugin
import json
from operator import itemgetter
from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf
from xmodule import word_cloud_block
# noinspection PyUnresolvedReferences
from xmodule.tests.helpers import override_descriptor_system # pylint: disable=unused-import
from xmodule.tests.helpers import override_descriptor_system, mock_render_template # pylint: disable=unused-import
from xmodule.x_module import STUDENT_VIEW
from .helpers import BaseTestXmodule
@pytest.mark.usefixtures("override_descriptor_system")
class TestWordCloud(BaseTestXmodule):
class _TestWordCloudBase(BaseTestXmodule):
"""Integration test for Word Cloud Block."""
__test__ = False
CATEGORY = "word_cloud"
@classmethod
def setUpClass(cls):
super().setUpClass()
plugin.PLUGIN_CACHE = {}
importlib.reload(word_cloud_block)
def setUp(self):
super().setUp()
self.request_factory = RequestFactoryNoCsrf()
def _get_users_state(self):
"""Return current state for each user:
@@ -27,7 +44,18 @@ class TestWordCloud(BaseTestXmodule):
users_state = {}
for user in self.users:
response = self.clients[user.username].post(self.get_url('get_state'))
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# The extracted Word Cloud XBlock uses @XBlock.json_handler, which expects a different
# request format and url pattern
handler_url = self.get_url('', handler_name='handle_get_state')
response = self.clients[user.username].post(
handler_url,
data=json.dumps({}),
content_type='application/json',
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
else:
response = self.clients[user.username].post(self.get_url('get_state'))
users_state[user.username] = json.loads(response.content.decode('utf-8'))
return users_state
@@ -40,11 +68,22 @@ class TestWordCloud(BaseTestXmodule):
users_state = {}
for user in self.users:
response = self.clients[user.username].post(
self.get_url('submit'),
{'student_words[]': words},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# The extracted Word Cloud XBlock uses @XBlock.json_handler, which expects a different
# request format and url pattern
handler_url = self.get_url('', handler_name='handle_submit_state')
response = self.clients[user.username].post(
handler_url,
data=json.dumps({'student_words': words}),
content_type='application/json',
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
)
else:
response = self.clients[user.username].post(
self.get_url('submit'),
{'student_words[]': words},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
users_state[user.username] = json.loads(response.content.decode('utf-8'))
return users_state
@@ -52,7 +91,6 @@ class TestWordCloud(BaseTestXmodule):
def _check_response(self, response_contents, correct_jsons):
"""Utility function that compares correct and real responses."""
for username, content in response_contents.items():
# Used in debugger for comparing objects.
# self.maxDiff = None
@@ -120,7 +158,6 @@ class TestWordCloud(BaseTestXmodule):
correct_state = {}
for index, user in enumerate(self.users):
correct_state[user.username] = {
'status': 'success',
'submitted': True,
@@ -202,6 +239,14 @@ class TestWordCloud(BaseTestXmodule):
for user in self.users
}
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# The extracted Word Cloud XBlock uses @XBlock.json_handler to handle AJAX requests,
# which automatically returns a 404 for unknown requests, so there's no need to test
# the incorrect dispatch case in this scenario.
for username, response in responses.items():
self.assertEqual(response.status_code, 404)
return
status_codes = {response.status_code for response in responses.values()}
assert status_codes.pop() == 200
@@ -214,19 +259,44 @@ class TestWordCloud(BaseTestXmodule):
}
)
def test_word_cloud_constructor(self):
@patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template)
def test_word_cloud_constructor(self, mock_render_django_template):
"""
Make sure that all parameters extracted correctly from xml.
"""
fragment = self.runtime.render(self.block, STUDENT_VIEW)
expected_context = {
'ajax_url': self.block.ajax_url,
'display_name': self.block.display_name,
'instructions': self.block.instructions,
'element_class': self.block.location.block_type,
'element_id': self.block.location.html_id(),
'element_class': self.block.scope_ids.block_type,
'num_inputs': 5, # default value
'submitted': False, # default value,
}
assert fragment.content == self.runtime.render_template('word_cloud.html', expected_context)
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
# If `USE_EXTRACTED_WORD_CLOUD_BLOCK` is enabled, the `expected_context` will be different
# because in the extracted Word Cloud XBlock, the expected context:
# - contains `range_num_inputs`
# - uses `UUID` for `element_id` instead of `html_id()`
# - does not include `ajax_url` since it uses the `@XBlock.json_handler` decorator for AJAX requests
expected_context['range_num_inputs'] = range(5)
uuid_str = re.search(r"UUID\('([a-f0-9\-]+)'\)", fragment.content).group(1)
expected_context['element_id'] = UUID(uuid_str)
mock_render_django_template.assert_called_once()
# Remove i18n service
fragment_content_clean = re.sub(r"\{.*?}", "{}", fragment.content)
assert fragment_content_clean == self.runtime.render_template('templates/word_cloud.html', expected_context)
else:
expected_context['ajax_url'] = self.block.ajax_url
expected_context['element_id'] = self.block.location.html_id()
assert fragment.content == self.runtime.render_template('word_cloud.html', expected_context)
@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