simplify css class management and internationalization of status strings (potentially) displayed to the user.
950 lines
38 KiB
Python
950 lines
38 KiB
Python
"""
|
|
Tests for the logic in input type mako templates.
|
|
"""
|
|
|
|
import unittest
|
|
import capa
|
|
import os.path
|
|
import json
|
|
from lxml import etree
|
|
from mako.template import Template as MakoTemplate
|
|
from mako import exceptions
|
|
from capa.inputtypes import Status
|
|
|
|
class TemplateError(Exception):
|
|
"""
|
|
Error occurred while rendering a Mako template.
|
|
"""
|
|
pass
|
|
|
|
|
|
class TemplateTestCase(unittest.TestCase):
|
|
"""
|
|
Utilitites 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
|
|
|
|
def setUp(self):
|
|
"""
|
|
Load the template under test.
|
|
"""
|
|
capa_path = capa.__path__[0]
|
|
self.template_path = os.path.join(capa_path,
|
|
'templates',
|
|
self.TEMPLATE_NAME)
|
|
with open(self.template_path) as f:
|
|
self.template = MakoTemplate(f.read())
|
|
|
|
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 = self.template.render_unicode(**context_dict)
|
|
except:
|
|
raise TemplateError(exceptions.text_error_template().render())
|
|
|
|
# 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("Could not parse XML from '{0}': {1}".format(
|
|
xml_str, str(exc)))
|
|
else:
|
|
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 = ("XML does not have %d match(es) for xpath '%s'\nXML: %s\nContext: %s"
|
|
% (exact_num, str(xpath), etree.tostring(xml_root), str(context_dict)))
|
|
|
|
self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=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)
|
|
self.assertTrue(len(element_list) > 0,
|
|
"Could not find element at '%s'" % str(xpath))
|
|
|
|
if exact:
|
|
self.assertEqual(text, element_list[0].text)
|
|
else:
|
|
self.assertIn(text, element_list[0].text)
|
|
|
|
|
|
class ChoiceGroupTemplateTest(TemplateTestCase):
|
|
"""
|
|
Test mako template for `<choicegroup>` input.
|
|
"""
|
|
|
|
TEMPLATE_NAME = 'choicegroup.html'
|
|
|
|
def setUp(self):
|
|
choices = [('1', 'choice 1'), ('2', 'choice 2'), ('3', 'choice 3')]
|
|
self.context = {'id': '1',
|
|
'choices': choices,
|
|
'status': Status('correct'),
|
|
'label': 'test',
|
|
'input_type': 'checkbox',
|
|
'name_array_suffix': '1',
|
|
'value': '3'}
|
|
super(ChoiceGroupTemplateTest, self).setUp()
|
|
|
|
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': 'radio', 'value': ''},
|
|
{'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': 'radio', 'value': ''},
|
|
{'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
|
|
(not 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[@class='choicegroup_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': '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[@class='choicegroup_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_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):
|
|
xml = self.render_to_xml(self.context)
|
|
xpath = "//fieldset[@aria-label='%s']" % self.context['label']
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
|
|
class TextlineTemplateTest(TemplateTestCase):
|
|
"""
|
|
Test mako template for `<textline>` input.
|
|
"""
|
|
|
|
TEMPLATE_NAME = 'textline.html'
|
|
|
|
def setUp(self):
|
|
self.context = {'id': '1',
|
|
'status': Status('correct'),
|
|
'label': 'test',
|
|
'value': '3',
|
|
'preprocessor': None,
|
|
'trailing_text': None}
|
|
super(TextlineTemplateTest, self).setUp()
|
|
|
|
def test_section_class(self):
|
|
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 = "//div[@class='%s']" % css_class
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
def test_status(self):
|
|
cases = [('correct', 'correct', 'correct'),
|
|
('unsubmitted', 'unanswered', 'unanswered'),
|
|
('incorrect', 'incorrect', 'incorrect'),
|
|
('incomplete', 'incorrect', 'incomplete')]
|
|
|
|
for (context_status, div_class, status_mark) 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 = "//div[@class='%s ']" % div_class
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
# Expect that we get a <p> with class="status"
|
|
# (used to by CSS to draw the green check / red x)
|
|
self.assert_has_text(xml, "//p[@class='status']",
|
|
status_mark, exact=False)
|
|
|
|
def test_label(self):
|
|
xml = self.render_to_xml(self.context)
|
|
xpath = "//input[@aria-label='%s']" % self.context['label']
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
def test_hidden(self):
|
|
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):
|
|
self.context['do_math'] = True
|
|
xml = self.render_to_xml(self.context)
|
|
|
|
xpath = "//input[@class='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):
|
|
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):
|
|
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):
|
|
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):
|
|
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 = "//div[@class='%s inline']" % div_class
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
def test_message(self):
|
|
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'])
|
|
|
|
|
|
class FormulaEquationInputTemplateTest(TemplateTestCase):
|
|
"""
|
|
Test make template for `<formulaequationinput>`s.
|
|
"""
|
|
TEMPLATE_NAME = 'formulaequationinput.html'
|
|
|
|
def setUp(self):
|
|
self.context = {
|
|
'id': 2,
|
|
'value': 'PREFILLED_VALUE',
|
|
'status': Status('unsubmitted'),
|
|
'label': 'test',
|
|
'previewer': 'file.js',
|
|
'reported_status': 'REPORTED_STATUS',
|
|
}
|
|
super(FormulaEquationInputTemplateTest, self).setUp()
|
|
|
|
def test_no_size(self):
|
|
xml = self.render_to_xml(self.context)
|
|
self.assert_no_xpath(xml, "//input[@size]", self.context)
|
|
|
|
def test_size(self):
|
|
self.context['size'] = '40'
|
|
xml = self.render_to_xml(self.context)
|
|
|
|
self.assert_has_xpath(xml, "//input[@size='40']", self.context)
|
|
|
|
class AnnotationInputTemplateTest(TemplateTestCase):
|
|
"""
|
|
Test mako template for `<annotationinput>` input.
|
|
"""
|
|
|
|
TEMPLATE_NAME = 'annotationinput.html'
|
|
|
|
def setUp(self):
|
|
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>', }
|
|
super(AnnotationInputTemplateTest, self).setUp()
|
|
|
|
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': '<p>Unescaped <b>HTML {0}</b></p>'.format(id_num)}
|
|
for id_num in range(0, 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(0, 5):
|
|
xpath = "//span[@data-id='{0}']/p/b".format(id_num)
|
|
self.assert_has_text(xml, xpath, 'HTML {0}'.format(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 = "//span[@class='status {0}']".format(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 mako template for `<mathstring>` input.
|
|
"""
|
|
|
|
TEMPLATE_NAME = 'mathstring.html'
|
|
|
|
def setUp(self):
|
|
self.context = {'isinline': False, 'mathstr': '', 'tail': ''}
|
|
super(MathStringTemplateTest, self).setUp()
|
|
|
|
def test_math_string_inline(self):
|
|
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):
|
|
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):
|
|
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 mako template for `<optioninput>` input.
|
|
"""
|
|
|
|
TEMPLATE_NAME = 'optioninput.html'
|
|
|
|
def setUp(self):
|
|
self.context = {
|
|
'id': 2,
|
|
'options': [],
|
|
'status': Status('unsubmitted'),
|
|
'label': 'test',
|
|
'value': 0
|
|
}
|
|
super(OptionInputTemplateTest, self).setUp()
|
|
|
|
def test_select_options(self):
|
|
|
|
# Create options 0-4, and select option 2
|
|
self.context['options'] = [(id_num, '<b>Option {0}</b>'.format(id_num))
|
|
for id_num in range(0, 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)
|
|
|
|
# Should have each of the options, with the correct description
|
|
# The description HTML should NOT be escaped
|
|
# (that's why we descend into the <b> tag)
|
|
for id_num in range(0, 5):
|
|
xpath = "//option[@value='{0}']/b".format(id_num)
|
|
self.assert_has_text(xml, xpath, 'Option {0}'.format(id_num))
|
|
|
|
# Should have the correct option selected
|
|
xpath = "//option[@selected='true']/b"
|
|
self.assert_has_text(xml, xpath, 'Option 2')
|
|
|
|
def test_status(self):
|
|
|
|
# Test cases, where each tuple represents
|
|
# `(input_status, expected_css_class)`
|
|
test_cases = [('unsubmitted', 'status unanswered'),
|
|
('correct', 'status correct'),
|
|
('incorrect', 'status incorrect'),
|
|
('incomplete', 'status incorrect')]
|
|
|
|
for (input_status, expected_css_class) in test_cases:
|
|
self.context['status'] = Status(input_status)
|
|
xml = self.render_to_xml(self.context)
|
|
|
|
xpath = "//span[@class='{0}']".format(expected_css_class)
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
def test_label(self):
|
|
xml = self.render_to_xml(self.context)
|
|
xpath = "//select[@aria-label='%s']" % self.context['label']
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
|
|
class DragAndDropTemplateTest(TemplateTestCase):
|
|
"""
|
|
Test mako template for `<draganddropinput>` input.
|
|
"""
|
|
|
|
TEMPLATE_NAME = 'drag_and_drop_input.html'
|
|
|
|
def setUp(self):
|
|
self.context = {'id': 2,
|
|
'drag_and_drop_json': '',
|
|
'value': 0,
|
|
'status': Status('unsubmitted'),
|
|
'msg': ''}
|
|
super(DragAndDropTemplateTest, self).setUp()
|
|
|
|
def test_status(self):
|
|
|
|
# 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 = "//div[@class='{0}']".format(expected_css_class)
|
|
self.assert_has_xpath(xml, xpath, self.context)
|
|
|
|
# Expect a <p> with the status
|
|
xpath = "//p[@class='status']"
|
|
self.assert_has_text(xml, xpath, expected_text, exact=False)
|
|
|
|
def test_drag_and_drop_json_html(self):
|
|
|
|
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 mako 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):
|
|
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',
|
|
'label': 'choicetext label',
|
|
'value': self.VALUE_DICT}
|
|
|
|
super(ChoiceTextGroupTemplateTest, self).setUp()
|
|
|
|
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,
|
|
"//{0}[@class='choicetextgroup_incorrect']".format(grouping_tag),
|
|
self.context)
|
|
|
|
self.assert_no_xpath(xml,
|
|
"//{0}[@class='choicetextgroup_correct']".format(grouping_tag),
|
|
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,
|
|
"//{0}[@class='choicetextgroup_incorrect']".format(grouping_tag),
|
|
self.context)
|
|
|
|
self.assert_no_xpath(xml,
|
|
"//{0}[@class='choicetextgroup_correct']".format(grouping_tag),
|
|
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'] = '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'] = '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_label(self):
|
|
xml = self.render_to_xml(self.context)
|
|
xpath = "//fieldset[@aria-label='%s']" % self.context['label']
|
|
self.assert_has_xpath(xml, xpath, self.context)
|