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