Files
edx-platform/xmodule/capa/tests/test_input_templates.py
2026-01-07 16:39:11 +05:00

1189 lines
45 KiB
Python

# pylint: disable=too-many-lines
"""
Tests for the logic in input type Django templates.
"""
import json
import traceback
import unittest
from collections import OrderedDict
from lxml import etree
from six.moves import range
from openedx.core.djangolib.markup import HTML
from xmodule.capa.inputtypes import Status
from xmodule.capa.tests.helpers import capa_render_template
from xmodule.stringify import stringify_children
class TemplateError(Exception):
"""
Error occurred while rendering a Django template.
"""
class TemplateTestCase(unittest.TestCase):
"""
Utilities for testing templates.
"""
# Subclasses override this to specify the file name of the template
# to be loaded from capa/templates.
# The template name should include the .html extension:
# for example: choicegroup.html
TEMPLATE_NAME = None
DESCRIBEDBY = 'aria-describedby="desc-1 desc-2"'
DESCRIPTIONS = OrderedDict(
[("desc-1", "description text 1"), ("desc-2", "<em>description</em> <mark>text</mark> 2")]
)
DESCRIPTION_IDS = " ".join(list(DESCRIPTIONS.keys()))
RESPONSE_DATA = {"label": "question text 101", "descriptions": DESCRIPTIONS}
def setUp(self):
"""
Initialize the context.
"""
super().setUp()
self.context = {}
def render_to_xml(self, context_dict):
"""
Render the template using the `context_dict` dict.
Returns an `etree` XML element.
"""
# add dummy STATIC_URL to template context
context_dict.setdefault("STATIC_URL", "/dummy-static/")
try:
xml_str = capa_render_template(self.TEMPLATE_NAME, context_dict)
except Exception as exc:
raise TemplateError(f"<pre>{traceback.format_exc()}</pre>") from exc
# Attempt to construct an XML tree from the template
# This makes it easy to use XPath to make assertions, rather
# than dealing with a string.
# We modify the string slightly by wrapping it in <test>
# tags, to ensure it has one root element.
try:
xml = etree.fromstring("<test>" + xml_str + "</test>")
except Exception as exc:
raise TemplateError(f"Could not parse XML from '{xml_str}': {exc}") from exc
return xml
def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1):
"""
Asserts that the xml tree has an element satisfying `xpath`.
`xml_root` is an etree XML element
`xpath` is an XPath string, such as `'/foo/bar'`
`context` is used to print a debugging message
`exact_num` is the exact number of matches to expect.
"""
message = (
f"XML does not have {exact_num} match(es) for xpath '{xpath}'\n"
f"XML: {etree.tostring(xml_root)}\nContext: {context_dict}"
)
assert len(xml_root.xpath(xpath)) == exact_num, message
def assert_no_xpath(self, xml_root, xpath, context_dict):
"""
Asserts that the xml tree does NOT have an element
satisfying `xpath`.
`xml_root` is an etree XML element
`xpath` is an XPath string, such as `'/foo/bar'`
`context` is used to print a debugging message
"""
self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0)
def assert_has_text(self, xml_root, xpath, text, exact=True):
"""
Find the element at `xpath` in `xml_root` and assert
that its text is `text`.
`xml_root` is an etree XML element
`xpath` is an XPath string, such as `'/foo/bar'`
`text` is the expected text that the element should contain
If multiple elements are found, checks the first one.
If no elements are found, the assertion fails.
"""
element_list = xml_root.xpath(xpath)
assert len(element_list) > 0, f"Could not find element at '{xpath}'\n{etree.tostring(xml_root)}"
if exact:
assert text == element_list[0].text.strip()
else:
assert text in element_list[0].text.strip()
def assert_description(self, describedby_xpaths):
"""
Verify that descriptions information is correct.
Arguments:
describedby_xpaths (list): list of xpaths to check aria-describedby attribute
"""
xml = self.render_to_xml(self.context)
# Verify that each description <p> tag has correct id, text and order
descriptions = OrderedDict(
(tag.get("id"), stringify_children(tag)) for tag in xml.xpath('//p[@class="question-description"]')
)
assert self.DESCRIPTIONS == descriptions
# for each xpath verify that description_ids are set correctly
for describedby_xpath in describedby_xpaths:
describedbys = xml.xpath(describedby_xpath)
# aria-describedby attributes must have ids
assert describedbys
for describedby in describedbys:
assert describedby == self.DESCRIPTION_IDS
def assert_describedby_attribute(self, describedby_xpaths):
"""
Verify that an element has no aria-describedby attribute if there are no descriptions.
Arguments:
describedby_xpaths (list): list of xpaths to check aria-describedby attribute
"""
self.context["describedby_html"] = ""
xml = self.render_to_xml(self.context)
# for each xpath verify that description_ids are set correctly
for describedby_xpath in describedby_xpaths:
describedbys = xml.xpath(describedby_xpath)
assert not describedbys
def assert_status(self, status_div=False, status_class=False):
"""
Verify status information.
Arguments:
status_div (bool): check presence of status div
status_class (bool): check presence of status class
"""
cases = [
("correct", "correct"),
("unsubmitted", "unanswered"),
("submitted", "submitted"),
("incorrect", "incorrect"),
("incomplete", "incorrect"),
]
for context_status, div_class in cases:
self.context["status"] = Status(context_status)
xml = self.render_to_xml(self.context)
# Expect that we get a <div> with correct class
if status_div:
xpath = f"//div[normalize-space(@class)='{div_class}']"
self.assert_has_xpath(xml, xpath, self.context)
# Expect that we get a <span> with class="status"
# (used to by CSS to draw the green check / red x)
self.assert_has_text(
xml,
f"//span[@class='status {div_class if status_class else ''}']/span[@class='sr']",
self.context["status"].display_name,
)
def assert_label(self, xpath=None, aria_label=False):
"""
Verify label is rendered correctly.
Arguments:
xpath (str): xpath expression for label element
aria_label (bool): check aria-label attribute value
"""
labels = [
{
"actual": "You see, but you do not observe. The distinction is clear.",
"expected": "You see, but you do not observe. The distinction is clear.",
},
{
"actual": "I choose to have <mark>faith</mark> because without that, I have <em>nothing</em>.",
"expected": "I choose to have faith because without that, I have nothing.",
},
]
response_data = {"response_data": {"descriptions": {}, "label": ""}}
self.context.update(response_data)
for label in labels:
self.context["response_data"]["label"] = label["actual"]
xml = self.render_to_xml(self.context)
if aria_label:
self.assert_has_xpath(xml, f"//*[@aria-label='{label['expected']}']", self.context)
else:
element_list = xml.xpath(xpath)
assert len(element_list) == 1
assert stringify_children(element_list[0]) == label["actual"]
class ChoiceGroupTemplateTest(TemplateTestCase):
"""
Test django template for `<choicegroup>` input.
"""
TEMPLATE_NAME = "choicegroup.html"
def setUp(self):
super().setUp()
choices = [("1", "choice 1"), ("2", "choice 2"), ("3", "choice 3")]
self.context = {
"id": "1",
"choices": choices,
"status": Status("correct"),
"input_type": "checkbox",
"name_array_suffix": "1",
"value": "3",
"response_data": self.RESPONSE_DATA,
"describedby_html": HTML(self.DESCRIBEDBY),
}
def test_problem_marked_correct(self):
"""
Test conditions under which the entire problem
(not a particular option) is marked correct.
"""
self.context["status"] = Status("correct")
self.context["input_type"] = "checkbox"
self.context["value"] = ["1", "2"]
# Should mark the entire problem correct
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator-container']/span[@class='status correct']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml, "//label[@class='choicegroup_incorrect']", self.context)
self.assert_no_xpath(xml, "//label[@class='choicegroup_correct']", self.context)
def test_problem_marked_incorrect(self):
"""
Test all conditions under which the entire problem
(not a particular option) is marked incorrect.
"""
conditions = [
{"status": Status("incorrect"), "input_type": "checkbox", "value": []},
{"status": Status("incorrect"), "input_type": "checkbox", "value": ["2"]},
{"status": Status("incorrect"), "input_type": "checkbox", "value": ["2", "3"]},
{"status": Status("incomplete"), "input_type": "checkbox", "value": []},
{"status": Status("incomplete"), "input_type": "checkbox", "value": ["2"]},
{"status": Status("incomplete"), "input_type": "checkbox", "value": ["2", "3"]},
]
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator-container']/span[@class='status incorrect']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml, "//label[@class='choicegroup_incorrect']", self.context)
self.assert_no_xpath(xml, "//label[@class='choicegroup_correct']", self.context)
def test_problem_marked_unsubmitted(self):
"""
Test all conditions under which the entire problem
(not a particular option) is marked unanswered.
"""
conditions = [
{"status": Status("unsubmitted"), "input_type": "radio", "value": ""},
{"status": Status("unsubmitted"), "input_type": "radio", "value": []},
{"status": Status("unsubmitted"), "input_type": "checkbox", "value": []},
{"input_type": "radio", "value": ""},
{"input_type": "radio", "value": []},
{"input_type": "checkbox", "value": []},
{"input_type": "checkbox", "value": ["1"]},
{"input_type": "checkbox", "value": ["1", "2"]},
]
self.context["status"] = Status("unanswered")
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator-container']/span[@class='status unanswered']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml, "//label[@class='choicegroup_incorrect']", self.context)
self.assert_no_xpath(xml, "//label[@class='choicegroup_correct']", self.context)
def test_option_marked_correct(self):
"""
Test conditions under which a particular option
and the entire problem is marked correct.
"""
conditions = [{"input_type": "radio", "value": "2"}, {"input_type": "radio", "value": ["2"]}]
self.context["status"] = Status("correct")
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//label[contains(@class, 'choicegroup_correct')]"
self.assert_has_xpath(xml, xpath, self.context)
# Should also mark the whole problem
xpath = "//div[@class='indicator-container']/span[@class='status correct']"
self.assert_has_xpath(xml, xpath, self.context)
def test_option_marked_incorrect(self):
"""
Test conditions under which a particular option
and the entire problem is marked incorrect.
"""
conditions = [{"input_type": "radio", "value": "2"}, {"input_type": "radio", "value": ["2"]}]
self.context["status"] = Status("incorrect")
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//label[contains(@class, 'choicegroup_incorrect')]"
self.assert_has_xpath(xml, xpath, self.context)
# Should also mark the whole problem
xpath = "//div[@class='indicator-container']/span[@class='status incorrect']"
self.assert_has_xpath(xml, xpath, self.context)
def test_never_show_correctness(self):
"""
Test conditions under which we tell the template to
NOT show correct/incorrect, but instead show a message.
This is used, for example, by the Justice course to ask
questions without specifying a correct answer. When
the student responds, the problem displays "Thank you
for your response"
"""
conditions = [
{"input_type": "radio", "status": Status("correct"), "value": ""},
{"input_type": "radio", "status": Status("correct"), "value": "2"},
{"input_type": "radio", "status": Status("correct"), "value": ["2"]},
{"input_type": "radio", "status": Status("incorrect"), "value": "2"},
{"input_type": "radio", "status": Status("incorrect"), "value": []},
{"input_type": "radio", "status": Status("incorrect"), "value": ["2"]},
{"input_type": "checkbox", "status": Status("correct"), "value": []},
{"input_type": "checkbox", "status": Status("correct"), "value": ["2"]},
{"input_type": "checkbox", "status": Status("incorrect"), "value": []},
{"input_type": "checkbox", "status": Status("incorrect"), "value": ["2"]},
]
self.context["show_correctness"] = "never"
self.context["submitted_message"] = "Test message"
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
# Should NOT mark the entire problem correct/incorrect
xpath = "//div[@class='indicator-container']/span[@class='status correct']"
self.assert_no_xpath(xml, xpath, self.context)
xpath = "//div[@class='indicator-container']/span[@class='status incorrect']"
self.assert_no_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml, "//label[@class='choicegroup_incorrect']", self.context)
self.assert_no_xpath(xml, "//label[@class='choicegroup_correct']", self.context)
# Expect to see the message
self.assert_has_text(xml, "//div[@class='capa_alert']", self.context["submitted_message"])
def test_no_message_before_submission(self):
"""
Ensure that we don't show the `submitted_message`
before submitting.
"""
conditions = [
{"input_type": "radio", "status": Status("unsubmitted"), "value": ""},
{"input_type": "radio", "status": Status("unsubmitted"), "value": []},
{"input_type": "checkbox", "status": Status("unsubmitted"), "value": []},
# These tests expose bug #365
# When the bug is fixed, uncomment these cases.
# {'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'},
# {'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']},
# {'input_type': 'radio', 'status': 'unsubmitted', 'value': '2'},
# {'input_type': 'radio', 'status': 'unsubmitted', 'value': ['2']},
# {'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']},
# {'input_type': 'checkbox', 'status': 'unsubmitted', 'value': ['2']}]
]
self.context["show_correctness"] = "never"
self.context["submitted_message"] = "Test message"
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
# Expect that we do NOT see the message yet
self.assert_no_xpath(xml, "//div[@class='capa_alert']", self.context)
def test_label(self):
"""
Verify label element value rendering.
"""
self.assert_label(xpath="//legend")
def test_description(self):
"""
Test that correct description information is set on desired elements.
"""
xpaths = ["//fieldset/@aria-describedby", "//label/@aria-describedby"]
self.assert_description(xpaths)
self.assert_describedby_attribute(xpaths)
def test_status(self):
"""
Verify status information.
"""
self.assert_status(status_class=True)
class TextlineTemplateTest(TemplateTestCase):
"""
Test django template for `<textline>` input.
"""
TEMPLATE_NAME = "textline.html"
def setUp(self):
super().setUp()
self.context = {
"id": "1",
"status": Status("correct"),
"value": "3",
"preprocessor": None,
"trailing_text": None,
"response_data": self.RESPONSE_DATA,
"describedby_html": HTML(self.DESCRIBEDBY),
}
def test_section_class(self):
"""Verify CSS classes for <textline> input under different context combinations."""
cases = [
({}, " capa_inputtype textline"),
({"do_math": True}, "text-input-dynamath capa_inputtype textline"),
({"inline": True}, " capa_inputtype inline textline"),
({"do_math": True, "inline": True}, "text-input-dynamath capa_inputtype inline textline"),
]
for context, css_class in cases:
base_context = self.context.copy()
base_context.update(context)
xml = self.render_to_xml(base_context)
xpath = f"//div[@class='{css_class}']"
self.assert_has_xpath(xml, xpath, self.context)
def test_status(self):
"""
Verify status information.
"""
self.assert_status(status_class=True)
def test_label(self):
"""
Verify label element value rendering.
"""
self.assert_label(xpath="//label[@class='problem-group-label']")
def test_hidden(self):
"""Ensure that hidden inputs and containers are rendered with display:none style."""
self.context["hidden"] = True
xml = self.render_to_xml(self.context)
xpath = "//div[@style='display:none;']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//input[@style='display:none;']"
self.assert_has_xpath(xml, xpath, self.context)
def test_do_math(self):
"""Verify that math-related elements and classes are rendered for do_math context."""
self.context["do_math"] = True
xml = self.render_to_xml(self.context)
xpath = "//input[@class='mw-100 math']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//div[@class='equation']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//textarea[@id='input_1_dynamath']"
self.assert_has_xpath(xml, xpath, self.context)
def test_size(self):
"""Ensure that the size attribute is correctly applied to input elements."""
self.context["size"] = "20"
xml = self.render_to_xml(self.context)
xpath = "//input[@size='20']"
self.assert_has_xpath(xml, xpath, self.context)
def test_preprocessor(self):
"""Verify that preprocessor-related data attributes are rendered correctly."""
self.context["preprocessor"] = {"class_name": "test_class", "script_src": "test_script"}
xml = self.render_to_xml(self.context)
xpath = "//div[contains(@class, 'text-input-dynamath_data') and @data-preprocessor='test_class']"
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//div[@class='script_placeholder' and @data-src='test_script']"
self.assert_has_xpath(xml, xpath, self.context)
def test_do_inline_and_preprocessor(self):
"""Ensure inline and preprocessor settings are applied together correctly."""
self.context["preprocessor"] = {"class_name": "test_class", "script_src": "test_script"}
self.context["inline"] = True
xml = self.render_to_xml(self.context)
xpath = "//div[contains(@class, 'text-input-dynamath_data inline') and @data-preprocessor='test_class']"
self.assert_has_xpath(xml, xpath, self.context)
def test_do_inline(self):
"""Verify inline class is applied correctly based on status context."""
cases = [
("correct", "correct"),
("unsubmitted", "unanswered"),
("incorrect", "incorrect"),
("incomplete", "incorrect"),
]
self.context["inline"] = True
for context_status, div_class in cases:
self.context["status"] = Status(context_status)
xml = self.render_to_xml(self.context)
# Expect that we get a <div> with correct class
xpath = f"//div[@class='{div_class} inline']"
self.assert_has_xpath(xml, xpath, self.context)
def test_message(self):
"""Check that message text is rendered inside the proper span element."""
self.context["msg"] = "Test message"
xml = self.render_to_xml(self.context)
xpath = "//span[@class='message']"
self.assert_has_text(xml, xpath, self.context["msg"])
def test_description(self):
"""
Test that correct description information is set on desired elements.
"""
xpaths = ["//input/@aria-describedby"]
self.assert_description(xpaths)
self.assert_describedby_attribute(xpaths)
class FormulaEquationInputTemplateTest(TemplateTestCase):
"""Test django template for `<formulaequationinput>` input."""
TEMPLATE_NAME = "formulaequationinput.html"
def setUp(self):
super().setUp()
self.context = {
"id": 2,
"value": "PREFILLED_VALUE",
"status": Status("unsubmitted"),
"previewer": "file.js",
"reported_status": "REPORTED_STATUS",
"trailing_text": None,
"response_data": self.RESPONSE_DATA,
"describedby_html": HTML(self.DESCRIBEDBY),
}
def test_no_size(self):
"""Ensure no size attribute is present when not specified in context."""
xml = self.render_to_xml(self.context)
self.assert_no_xpath(xml, "//input[@size]", self.context)
def test_size(self):
"""Verify that size attribute is correctly applied to formula equation input."""
self.context["size"] = "40"
xml = self.render_to_xml(self.context)
self.assert_has_xpath(xml, "//input[@size='40']", self.context)
def test_description(self):
"""
Test that correct description information is set on desired elements.
"""
xpaths = ["//input/@aria-describedby"]
self.assert_description(xpaths)
self.assert_describedby_attribute(xpaths)
def test_status(self):
"""
Verify status information.
"""
self.assert_status(status_class=True)
def test_label(self):
"""
Verify label element value rendering.
"""
self.assert_label(xpath="//label[@class='problem-group-label']")
class AnnotationInputTemplateTest(TemplateTestCase):
"""
Test django template for `<annotationinput>` input.
"""
TEMPLATE_NAME = "annotationinput.html"
def setUp(self):
super().setUp()
self.context = {
"id": 2,
"value": "<p>Test value</p>",
"title": "<h1>This is a title</h1>",
"text": "<p><b>This</b> is a test.</p>",
"comment": "<p>This is a test comment</p>",
"comment_prompt": "<p>This is a test comment prompt</p>",
"comment_value": "<p>This is the value of a test comment</p>",
"tag_prompt": "<p>This is a tag prompt</p>",
"options": [],
"has_options_value": False,
"debug": False,
"status": Status("unsubmitted"),
"return_to_annotation": False,
"msg": "<p>This is a test message</p>",
}
def test_return_to_annotation(self):
"""
Test link for `Return to Annotation` appears if and only if
the flag is set.
"""
xpath = "//a[@class='annotation-return']"
# If return_to_annotation set, then show the link
self.context["return_to_annotation"] = True
xml = self.render_to_xml(self.context)
self.assert_has_xpath(xml, xpath, self.context)
# Otherwise, do not show the links
self.context["return_to_annotation"] = False
xml = self.render_to_xml(self.context)
self.assert_no_xpath(xml, xpath, self.context)
def test_option_selection(self):
"""
Test that selected options are selected.
"""
# Create options 0-4 and select option 2
self.context["options_value"] = [2]
self.context["options"] = [
{"id": id_num, "choice": "correct", "description": f"<p>Unescaped <b>HTML {id_num}</b></p>"}
for id_num in range(5)
]
xml = self.render_to_xml(self.context)
# Expect that each option description is visible
# with unescaped HTML.
# Since the HTML is unescaped, we can traverse the XML tree
for id_num in range(5):
xpath = f"//span[@data-id='{id_num}']/p/b"
self.assert_has_text(xml, xpath, f"HTML {id_num}", exact=False)
# Expect that the correct option is selected
xpath = "//span[contains(@class,'selected')]/p/b"
self.assert_has_text(xml, xpath, "HTML 2", exact=False)
def test_submission_status(self):
"""
Test that the submission status displays correctly.
"""
# Test cases of `(input_status, expected_css_class)` tuples
test_cases = [("unsubmitted", "unanswered"), ("incomplete", "incorrect"), ("incorrect", "incorrect")]
for input_status, expected_css_class in test_cases:
self.context["status"] = Status(input_status)
xml = self.render_to_xml(self.context)
xpath = f"//span[@class='status {expected_css_class}']"
self.assert_has_xpath(xml, xpath, self.context)
# If individual options are being marked, then expect
# just the option to be marked incorrect, not the whole problem
self.context["has_options_value"] = True
self.context["status"] = Status("incorrect")
xpath = "//span[@class='incorrect']"
xml = self.render_to_xml(self.context)
self.assert_no_xpath(xml, xpath, self.context)
def test_display_html_comment(self):
"""
Test that HTML comment and comment prompt render.
"""
self.context["comment"] = "<p>Unescaped <b>comment HTML</b></p>"
self.context["comment_prompt"] = "<p>Prompt <b>prompt HTML</b></p>"
self.context["text"] = "<p>Unescaped <b>text</b></p>"
xml = self.render_to_xml(self.context)
# Because the HTML is unescaped, we should be able to
# descend to the <b> tag
xpath = "//div[@class='block']/p/b"
self.assert_has_text(xml, xpath, "prompt HTML")
xpath = "//div[@class='block block-comment']/p/b"
self.assert_has_text(xml, xpath, "comment HTML")
xpath = "//div[@class='block block-highlight']/p/b"
self.assert_has_text(xml, xpath, "text")
def test_display_html_tag_prompt(self):
"""
Test that HTML tag prompts render.
"""
self.context["tag_prompt"] = "<p>Unescaped <b>HTML</b></p>"
xml = self.render_to_xml(self.context)
# Because the HTML is unescaped, we should be able to
# descend to the <b> tag
xpath = "//div[@class='block']/p/b"
self.assert_has_text(xml, xpath, "HTML")
class MathStringTemplateTest(TemplateTestCase):
"""
Test django template for `<mathstring>` input.
"""
TEMPLATE_NAME = "mathstring.html"
def setUp(self):
super().setUp()
self.context = {"isinline": False, "mathstr": "", "tail": ""}
def test_math_string_inline(self):
"""Verify that math string is rendered inline correctly with MathJax tags."""
self.context["isinline"] = True
self.context["mathstr"] = "y = ax^2 + bx + c"
xml = self.render_to_xml(self.context)
xpath = "//section[@class='math-string']/span[1]"
self.assert_has_text(xml, xpath, "[mathjaxinline]y = ax^2 + bx + c[/mathjaxinline]")
def test_math_string_not_inline(self):
"""Verify that math string is rendered as block (not inline) correctly with MathJax tags."""
self.context["isinline"] = False
self.context["mathstr"] = "y = ax^2 + bx + c"
xml = self.render_to_xml(self.context)
xpath = "//section[@class='math-string']/span[1]"
self.assert_has_text(xml, xpath, "[mathjax]y = ax^2 + bx + c[/mathjax]")
def test_tail_html(self):
"""Ensure that tail HTML is rendered correctly and unescaped."""
self.context["tail"] = "<p>This is some <b>tail</b> <em>HTML</em></p>"
xml = self.render_to_xml(self.context)
# HTML from `tail` should NOT be escaped.
# We should be able to traverse it as part of the XML tree
xpath = "//section[@class='math-string']/span[2]/p/b"
self.assert_has_text(xml, xpath, "tail")
xpath = "//section[@class='math-string']/span[2]/p/em"
self.assert_has_text(xml, xpath, "HTML")
class OptionInputTemplateTest(TemplateTestCase):
"""
Test django template for `<optioninput>` input.
"""
TEMPLATE_NAME = "optioninput.html"
def setUp(self):
super().setUp()
self.context = {
"id": 2,
"options": [],
"status": Status("unsubmitted"),
"value": 0,
"default_option_text": "Select an option",
"response_data": self.RESPONSE_DATA,
"describedby_html": HTML(self.DESCRIBEDBY),
}
def test_select_options(self):
"""Verify that option elements are rendered correctly and the selected option is marked."""
# Create options 0-4, and select option 2
self.context["options"] = [(id_num, f"Option {id_num}") for id_num in range(5)]
self.context["value"] = 2
xml = self.render_to_xml(self.context)
# Should have a dummy default
xpath = "//option[@value='option_2_dummy_default']"
self.assert_has_xpath(xml, xpath, self.context)
for id_num in range(5):
xpath = f"//option[@value='{id_num}']"
self.assert_has_text(xml, xpath, f"Option {id_num}")
# Should have the correct option selected
xpath = "//option[@selected='true']"
self.assert_has_text(xml, xpath, "Option 2")
def test_status(self):
"""
Verify status information.
"""
self.assert_status(status_class=True)
def test_label(self):
"""
Verify label element value rendering.
"""
self.assert_label(xpath="//label[@class='problem-group-label']")
def test_description(self):
"""
Test that correct description information is set on desired elements.
"""
xpaths = ["//select/@aria-describedby"]
self.assert_description(xpaths)
self.assert_describedby_attribute(xpaths)
class DragAndDropTemplateTest(TemplateTestCase):
"""
Test django template for `<draganddropinput>` input.
"""
TEMPLATE_NAME = "drag_and_drop_input.html"
def setUp(self):
super().setUp()
self.context = {"id": 2, "drag_and_drop_json": "", "value": 0, "status": Status("unsubmitted"), "msg": ""}
def test_status(self):
"""Verify that drag-and-drop input renders correct status CSS classes and text."""
# Test cases, where each tuple represents
# `(input_status, expected_css_class, expected_text)`
test_cases = [
("unsubmitted", "unanswered", "unanswered"),
("correct", "correct", "correct"),
("incorrect", "incorrect", "incorrect"),
("incomplete", "incorrect", "incomplete"),
]
for input_status, expected_css_class, expected_text in test_cases:
self.context["status"] = Status(input_status)
xml = self.render_to_xml(self.context)
# Expect a <div> with the status
xpath = f"//div[@class='{expected_css_class}']"
self.assert_has_xpath(xml, xpath, self.context)
# Expect a <span> with the status
xpath = f"//span[@class='status {expected_css_class}']/span[@class='sr']"
self.assert_has_text(xml, xpath, expected_text, exact=False)
def test_drag_and_drop_json_html(self):
"""Ensure that drag-and-drop JSON with HTML is rendered without escaping HTML."""
json_with_html = json.dumps({"test": "<p>Unescaped <b>HTML</b></p>"})
self.context["drag_and_drop_json"] = json_with_html
xml = self.render_to_xml(self.context)
# Assert that the JSON-encoded string was inserted without
# escaping the HTML. We should be able to traverse the XML tree.
xpath = "//div[@class='drag_and_drop_problem_json']/p/b"
self.assert_has_text(xml, xpath, "HTML")
class ChoiceTextGroupTemplateTest(TemplateTestCase):
"""Test django template for `<choicetextgroup>` input"""
TEMPLATE_NAME = "choicetext.html"
VALUE_DICT = {
"1_choiceinput_0bc": "1_choiceinput_0bc",
"1_choiceinput_0_textinput_0": "0",
"1_choiceinput_1_textinput_0": "0",
}
EMPTY_DICT = {"1_choiceinput_0_textinput_0": "", "1_choiceinput_1_textinput_0": ""}
BOTH_CHOICE_CHECKBOX = {
"1_choiceinput_0bc": "choiceinput_0",
"1_choiceinput_1bc": "choiceinput_1",
"1_choiceinput_0_textinput_0": "0",
"1_choiceinput_1_textinput_0": "0",
}
WRONG_CHOICE_CHECKBOX = {
"1_choiceinput_1bc": "choiceinput_1",
"1_choiceinput_0_textinput_0": "0",
"1_choiceinput_1_textinput_0": "0",
}
def setUp(self):
super().setUp()
choices = [
(
"1_choiceinput_0bc",
[
{"tail_text": "", "type": "text", "value": "", "contents": ""},
{"tail_text": "", "type": "textinput", "value": "", "contents": "choiceinput_0_textinput_0"},
],
),
(
"1_choiceinput_1bc",
[
{"tail_text": "", "type": "text", "value": "", "contents": ""},
{"tail_text": "", "type": "textinput", "value": "", "contents": "choiceinput_1_textinput_0"},
],
),
]
self.context = {
"id": "1",
"choices": choices,
"status": Status("correct"),
"input_type": "radio",
"value": self.VALUE_DICT,
"response_data": self.RESPONSE_DATA,
}
def test_grouping_tag(self):
"""
Tests whether we are using a section or a label to wrap choice elements.
Section is used for checkbox, so inputting text does not deselect
"""
input_tags = ("radio", "checkbox")
self.context["status"] = Status("correct")
xpath = "//section[@id='forinput1_choiceinput_0bc']"
self.context["value"] = {}
for input_type in input_tags:
self.context["input_type"] = input_type
xml = self.render_to_xml(self.context)
self.assert_has_xpath(xml, xpath, self.context)
def test_problem_marked_correct(self):
"""Test conditions under which the entire problem
(not a particular option) is marked correct"""
self.context["status"] = Status("correct")
self.context["input_type"] = "checkbox"
self.context["value"] = self.VALUE_DICT
# Should mark the entire problem correct
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator-container']/span[@class='status correct']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
self.assert_no_xpath(xml, "//label[@class='choicetextgroup_incorrect']", self.context)
self.assert_no_xpath(xml, "//label[@class='choicetextgroup_correct']", self.context)
def test_problem_marked_incorrect(self):
"""Test all conditions under which the entire problem
(not a particular option) is marked incorrect"""
grouping_tags = {"radio": "label", "checkbox": "section"}
conditions = [
{"status": Status("incorrect"), "input_type": "radio", "value": {}},
{"status": Status("incorrect"), "input_type": "checkbox", "value": self.WRONG_CHOICE_CHECKBOX},
{"status": Status("incorrect"), "input_type": "checkbox", "value": self.BOTH_CHOICE_CHECKBOX},
{"status": Status("incorrect"), "input_type": "checkbox", "value": self.VALUE_DICT},
{"status": Status("incomplete"), "input_type": "radio", "value": {}},
{"status": Status("incomplete"), "input_type": "checkbox", "value": self.WRONG_CHOICE_CHECKBOX},
{"status": Status("incomplete"), "input_type": "checkbox", "value": self.BOTH_CHOICE_CHECKBOX},
{"status": Status("incomplete"), "input_type": "checkbox", "value": self.VALUE_DICT},
]
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator-container']/span[@class='status incorrect']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
grouping_tag = grouping_tags[test_conditions["input_type"]]
self.assert_no_xpath(xml, f"//{grouping_tag}[@class='choicetextgroup_incorrect']", self.context)
self.assert_no_xpath(xml, f"//{grouping_tag}[@class='choicetextgroup_correct']", self.context)
def test_problem_marked_unsubmitted(self):
"""Test all conditions under which the entire problem
(not a particular option) is marked unanswered"""
grouping_tags = {"radio": "label", "checkbox": "section"}
conditions = [
{"status": Status("unsubmitted"), "input_type": "radio", "value": {}},
{"status": Status("unsubmitted"), "input_type": "radio", "value": self.EMPTY_DICT},
{"status": Status("unsubmitted"), "input_type": "checkbox", "value": {}},
{"status": Status("unsubmitted"), "input_type": "checkbox", "value": self.EMPTY_DICT},
{"status": Status("unsubmitted"), "input_type": "checkbox", "value": self.VALUE_DICT},
{"status": Status("unsubmitted"), "input_type": "checkbox", "value": self.BOTH_CHOICE_CHECKBOX},
]
self.context["status"] = Status("unanswered")
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//div[@class='indicator-container']/span[@class='status unanswered']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark individual options
grouping_tag = grouping_tags[test_conditions["input_type"]]
self.assert_no_xpath(xml, f"//{grouping_tag}[@class='choicetextgroup_incorrect']", self.context)
self.assert_no_xpath(xml, f"//{grouping_tag}[@class='choicetextgroup_correct']", self.context)
def test_option_marked_correct(self):
"""Test conditions under which a particular option
(not the entire problem) is marked correct."""
conditions = [{"input_type": "radio", "value": self.VALUE_DICT}]
self.context["status"] = Status("correct")
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//section[@id='forinput1_choiceinput_0bc' and\
@class='choicetextgroup_correct']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark the whole problem
xpath = "//div[@class='indicator-container']/span"
self.assert_no_xpath(xml, xpath, self.context)
def test_option_marked_incorrect(self):
"""Test conditions under which a particular option
(not the entire problem) is marked incorrect."""
conditions = [{"input_type": "radio", "value": self.VALUE_DICT}]
self.context["status"] = Status("incorrect")
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//section[@id='forinput1_choiceinput_0bc' and\
@class='choicetextgroup_incorrect']"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark the whole problem
xpath = "//div[@class='indicator-container']/span"
self.assert_no_xpath(xml, xpath, self.context)
def test_aria_label(self):
"""
Verify aria-label attribute rendering.
"""
self.assert_label(aria_label=True)
class ChemicalEquationTemplateTest(TemplateTestCase):
"""Test django template for `<chemicalequationinput>` input"""
TEMPLATE_NAME = "chemicalequationinput.html"
def setUp(self):
super().setUp()
self.context = {
"id": "1",
"status": Status("correct"),
"previewer": "dummy.js",
"value": "101",
}
def test_aria_label(self):
"""
Verify aria-label attribute rendering.
"""
self.assert_label(aria_label=True)
class SchematicInputTemplateTest(TemplateTestCase):
"""Test django template for `<schematic>` input"""
TEMPLATE_NAME = "schematicinput.html"
def setUp(self):
super().setUp()
self.context = {
"id": "1",
"status": Status("correct"),
"previewer": "dummy.js",
"value": "101",
"STATIC_URL": "/dummy-static/",
"msg": "",
"initial_value": "two large batteries",
"width": "100",
"height": "100",
"parts": "resistors, capacitors, and flowers",
"setup_script": "/dummy-static/js/capa/schematicinput.js",
"analyses": "fast, slow, and pink",
"submit_analyses": "maybe",
}
def test_aria_label(self):
"""
Verify aria-label attribute rendering.
"""
self.assert_label(aria_label=True)
class CodeinputTemplateTest(TemplateTestCase):
"""
Test django template for `<textbox>` input
"""
TEMPLATE_NAME = "codeinput.html"
def setUp(self):
super().setUp()
self.context = {
"id": "1",
"status": Status("correct"),
"mode": "parrot",
"linenumbers": "false",
"rows": "37",
"cols": "11",
"tabsize": "7",
"hidden": "",
"msg": "",
"value": 'print "good evening"',
"aria_label": "python editor",
"code_mirror_exit_message": "Press ESC then TAB or click outside of the code editor to exit",
"response_data": self.RESPONSE_DATA,
"describedby": HTML(self.DESCRIBEDBY),
}
def test_label(self):
"""
Verify question label is rendered correctly.
"""
self.assert_label(xpath="//label[@class='problem-group-label']")
def test_editor_exit_message(self):
"""
Verify that editor exit message is rendered.
"""
xml = self.render_to_xml(self.context)
self.assert_has_text(xml, '//span[@id="cm-editor-exit-message-1"]', self.context["code_mirror_exit_message"])