1189 lines
45 KiB
Python
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"])
|