diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 7edfd4f1a1..b18e26fd0e 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -592,8 +592,12 @@ class JSInput(InputTypeBase):
# set state
Attribute('width', "400"), # iframe width
Attribute('height', "300"), # iframe height
- Attribute('sop', None) # SOP will be relaxed only if this
- # attribute is set to false.
+ # Title for the iframe, which should be supplied by the author of the problem. Not translated
+ # because we are in a class method and therefore do not have access to capa_system.i18n.
+ # Note that the default "display name" for the problem is also not translated.
+ Attribute('title', "Problem Remote Content"),
+ # SOP will be relaxed only if this attribute is set to false.
+ Attribute('sop', None)
]
def _extra_context(self):
diff --git a/common/lib/capa/capa/templates/jsinput.html b/common/lib/capa/capa/templates/jsinput.html
index e1ee34f0a5..7c108817a8 100644
--- a/common/lib/capa/capa/templates/jsinput.html
+++ b/common/lib/capa/capa/templates/jsinput.html
@@ -1,11 +1,12 @@
+<%page expression_filter="h"/>
<%! from openedx.core.djangolib.markup import HTML %>
-
+ value="${value}"/>
@@ -54,4 +56,4 @@
% if msg:
${HTML(msg)}
% endif
-
+
diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py
index 12dc8184e9..5bc657d9e4 100644
--- a/common/lib/capa/capa/tests/response_xml_factory.py
+++ b/common/lib/capa/capa/tests/response_xml_factory.py
@@ -608,6 +608,18 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
return input_element
+class JSInputXMLFactory(CustomResponseXMLFactory):
+ """
+ Factory for producing XML.
+ Note that this factory currently does not create a functioning problem.
+ It will only create an empty iframe.
+ """
+
+ def create_input_element(self, **kwargs):
+ """ Create the element """
+ return etree.Element("jsinput")
+
+
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing XML """
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py
index 959cadef91..851de494cb 100644
--- a/common/lib/capa/capa/tests/test_inputtypes.py
+++ b/common/lib/capa/capa/tests/test_inputtypes.py
@@ -162,6 +162,89 @@ class ChoiceGroupTest(unittest.TestCase):
self.check_group('checkboxgroup', 'checkbox', '[]')
+class JSInputTest(unittest.TestCase):
+ """
+ Test context variables passed into the jsinput template.
+ """
+
+ def test_rendering_default_values(self):
+ """
+ Tests the default values passed through to render.
+ """
+ xml_str = ''
+ expected = {
+ 'html_file': None,
+ 'gradefn': "gradefn",
+ 'get_statefn': None,
+ 'set_statefn': None,
+ 'initial_state': None,
+ 'width': "400",
+ 'height': "300",
+ 'title': "Problem Remote Content",
+ 'sop': None
+ }
+
+ self._render_context_test(xml_str, expected)
+
+ def test_rendering_provided_values(self):
+ """
+ Tests that values provided by course authors are passed through to render.
+ """
+ xml_str = """
+
+ """
+
+ expected = {
+ 'html_file': "https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html",
+ 'gradefn': "WebGLDemo.getGrade",
+ 'get_statefn': "WebGLDemo.getState",
+ 'set_statefn': "WebGLDemo.setState",
+ 'initial_state': '{"selectedObjects":{"cube":true,"cylinder":false}}',
+ 'width': "1000",
+ 'height': "1200",
+ 'title': "Awesome and fun!",
+ 'sop': 'false'
+ }
+
+ self._render_context_test(xml_str, expected)
+
+ def _render_context_test(self, xml_str, expected_context):
+ """
+ Helper method for testing context based on the provided XML string.
+ """
+ element = etree.fromstring(xml_str)
+ state = {
+ 'value': 103,
+ 'response_data': RESPONSE_DATA
+ }
+ the_input = lookup_tag('jsinput')(test_capa_system(), element, state)
+
+ context = the_input._get_render_context() # pylint: disable=protected-access
+
+ full_expected_context = {
+ 'STATIC_URL': '/dummy-static/',
+ 'id': 'prob_1_2',
+ 'status': inputtypes.Status('unanswered'),
+ 'describedby_html': DESCRIBEDBY.format(status_id='prob_1_2'),
+ 'msg': "",
+ 'params': None,
+ 'jschannel_loader': '/dummy-static/js/capa/src/jschannel.js',
+ 'jsinput_loader': '/dummy-static/js/capa/src/jsinput.js',
+ 'saved_state': 103,
+ 'response_data': RESPONSE_DATA,
+ 'value': 103
+ }
+ full_expected_context.update(expected_context)
+
+ self.assertEqual(full_expected_context, context)
+
+
class TextLineTest(unittest.TestCase):
'''
Check that textline inputs work, with and without math.
diff --git a/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml b/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml
index 7e44026eb2..5a37b6ede3 100644
--- a/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml
@@ -66,6 +66,7 @@ data: |
width="400"
height="400"
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html"
+ title="Spinning Cone and Cube"
sop="false"/>
diff --git a/common/static/js/capa/spec/jsinput_spec.js b/common/static/js/capa/spec/jsinput_spec.js
index 38a2feb442..1783ef3018 100644
--- a/common/static/js/capa/spec/jsinput_spec.js
+++ b/common/static/js/capa/spec/jsinput_spec.js
@@ -1,29 +1,29 @@
describe('JSInput', function() {
- var sections;
- var inputFields;
+ var $jsinputContainers;
+ var $inputFields;
beforeEach(function() {
loadFixtures('js/capa/fixtures/jsinput.html');
- sections = $('section[id^="inputtype_"]');
- inputFields = $('input[id^="input_"]');
+ $jsinputContainers = $('.jsinput');
+ $inputFields = $('input[id^="input_"]');
JSInput.walkDOM();
});
it('sets all data-processed attributes to true on first load', function() {
- sections.each(function(index, item) {
+ $jsinputContainers.each(function(index, item) {
expect(item).toHaveData('processed', true);
});
});
it('sets the waitfor attribute to its update function', function() {
- inputFields.each(function(index, item) {
+ $inputFields.each(function(index, item) {
expect(item).toHaveAttr('waitfor');
});
});
- it('tests the correct number of sections', function() {
- expect(sections.length).toEqual(2);
- expect(sections.length).toEqual(inputFields.length);
+ it('tests the correct number of jsinput instances', function() {
+ expect($jsinputContainers.length).toEqual(2);
+ expect($jsinputContainers.length).toEqual($inputFields.length);
});
});
diff --git a/common/static/js/capa/src/jsinput.js b/common/static/js/capa/src/jsinput.js
index 1870e29ae3..35c64c5580 100644
--- a/common/static/js/capa/src/jsinput.js
+++ b/common/static/js/capa/src/jsinput.js
@@ -39,26 +39,26 @@ var JSInput = (function($, undefined) {
/* Private methods */
- var section = $(elem).parent().find('section[class="jsinput"]'),
- sectionAttr = function(e) { return $(section).attr(e); },
+ var jsinputContainer = $(elem).parent().find('.jsinput'),
+ jsinputAttr = function(e) { return $(jsinputContainer).attr(e); },
iframe = $(elem).find('iframe[name^="iframe_"]').get(0),
cWindow = iframe.contentWindow,
path = iframe.src.substring(0, iframe.src.lastIndexOf('/') + 1),
// Get the hidden input field to pass to customresponse
inputField = $(elem).parent().find('input[id^="input_"]'),
// Get the grade function name
- gradeFn = sectionAttr('data'),
+ gradeFn = jsinputAttr('data'),
// Get state getter
- stateGetter = sectionAttr('data-getstate'),
+ stateGetter = jsinputAttr('data-getstate'),
// Get state setter
- stateSetter = sectionAttr('data-setstate'),
+ stateSetter = jsinputAttr('data-setstate'),
// Get stored state
- storedState = sectionAttr('data-stored'),
+ storedState = jsinputAttr('data-stored'),
// Get initial state
- initialState = sectionAttr('data-initial-state'),
+ initialState = jsinputAttr('data-initial-state'),
// Bypass single-origin policy only if this attribute is "false"
// In that case, use JSChannel to do so.
- sop = sectionAttr('data-sop'),
+ sop = jsinputAttr('data-sop'),
channel;
sop = (sop !== 'false');
@@ -189,14 +189,14 @@ var JSInput = (function($, undefined) {
}
function walkDOM() {
- var allSections = $('section.jsinput');
+ var $jsinputContainers = $('.jsinput');
// When a JSInput problem loads, its data-processed attribute is false,
// so the jsconstructor will be called for it.
// The constructor will not be called again on subsequent reruns of
// this file by other JSInput. Only if it is reloaded, either with the
// rest of the page or when it is submitted, will this constructor be
// called again.
- allSections.each(function(index, value) {
+ $jsinputContainers.each(function(index, value) {
var dataProcessed = ($(value).attr('data-processed') === 'true');
if (!dataProcessed) {
jsinputConstructor(value);
diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py
index 3787d3fa4d..2276043d4b 100644
--- a/common/test/acceptance/tests/lms/test_problem_types.py
+++ b/common/test/acceptance/tests/lms/test_problem_types.py
@@ -19,6 +19,7 @@ from capa.tests.response_xml_factory import (
CustomResponseXMLFactory,
FormulaResponseXMLFactory,
ImageResponseXMLFactory,
+ JSInputXMLFactory,
MultipleChoiceResponseXMLFactory,
NumericalResponseXMLFactory,
OptionResponseXMLFactory,
@@ -132,7 +133,29 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin):
raise NotImplementedError()
-class ProblemTypeTestMixin(object):
+class ProblemTypeA11yTestMixin(object):
+ """
+ Shared a11y tests for all problem types.
+ """
+ @attr('a11y')
+ def test_problem_type_a11y(self):
+ """
+ Run accessibility audit for the problem type.
+ """
+ self.problem_page.wait_for(
+ lambda: self.problem_page.problem_name == self.problem_name,
+ "Make sure the correct problem is on the page"
+ )
+
+ # Set the scope to the problem container
+ self.problem_page.a11y_audit.config.set_scope(
+ include=['div#seq_content'])
+
+ # Run the accessibility audit.
+ self.problem_page.a11y_audit.check_for_accessibility_errors()
+
+
+class ProblemTypeTestMixin(ProblemTypeA11yTestMixin):
"""
Test cases shared amongst problem types.
"""
@@ -357,23 +380,6 @@ class ProblemTypeTestMixin(object):
self.problem_page.click_submit()
self.problem_page.wait_partial_notification()
- @attr('a11y')
- def test_problem_type_a11y(self):
- """
- Run accessibility audit for the problem type.
- """
- self.problem_page.wait_for(
- lambda: self.problem_page.problem_name == self.problem_name,
- "Make sure the correct problem is on the page"
- )
-
- # Set the scope to the problem container
- self.problem_page.a11y_audit.config.set_scope(
- include=['div#seq_content'])
-
- # Run the accessibility audit.
- self.problem_page.a11y_audit.check_for_accessibility_errors()
-
class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
"""
@@ -801,6 +807,29 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.fill_answer(second_addend, input_num=1)
+class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin):
+ """
+ TestCase Class for jsinput (custom JavaScript) problem type.
+ Right now the only test point that is executed is the a11y test.
+ This is because the factory simply creates an empty iframe.
+ """
+ problem_name = 'JSINPUT PROBLEM'
+ problem_type = 'customresponse'
+
+ factory = JSInputXMLFactory()
+
+ factory_kwargs = {
+ 'question_text': 'IFrame shows below (but has no content)'
+ }
+
+ def answer_problem(self, correctness):
+ """
+ Problem is not set up to work (displays an empty iframe), but this method must
+ be extended because the parent class has marked it as abstract.
+ """
+ raise NotImplementedError()
+
+
class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
"""
TestCase Class for Code Problem Type