Files
edx-platform/xmodule/js/spec/capa/display_spec.js
Irtaza Akram c3e85426cb Autoformat Problem XBlock Source Files for Consistency (2/2) (#37487)
* fix: run prettier on problem block code

* fix: codeql issues
2025-12-08 20:01:42 +05:00

1177 lines
43 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
describe("Problem", function () {
const problem_content_default = readFixtures("problem_content.html");
beforeEach(function () {
// Stub MathJax
window.MathJax = {
Hub: jasmine.createSpyObj("MathJax.Hub", ["getAllJax", "Queue"]),
Callback: jasmine.createSpyObj("MathJax.Callback", ["After"]),
};
this.stubbedJax = { root: jasmine.createSpyObj("jax.root", ["toMathML"]) };
MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
window.update_schematics = function () {};
spyOn(SR, "readText");
spyOn(SR, "readTexts");
// Load this function from spec/helper.js
// Note that if your test fails with a message like:
// 'External request attempted for blah, which is not defined.'
// this msg is coming from the stubRequests function else clause.
jasmine.stubRequests();
loadFixtures("problem.html");
spyOn(Logger, "log");
spyOn($.fn, "load").and.callFake(function (url, callback) {
$(this).html(readFixtures("problem_content.html"));
return callback();
});
});
describe("constructor", function () {
it("set the element from html", function () {
this.problem999 = new Problem(`\
<section class='xblock xblock-student_view xmodule_display xmodule_CapaModule' data-type='Problem'> \
<section id='problem_999' \
class='problems-wrapper' \
data-problem-id='i4x://edX/999/problem/Quiz' \
data-url='/problem/quiz/'> \
</section> \
</section>\
`);
expect(this.problem999.element_id).toBe("problem_999");
});
it("set the element from loadFixtures", function () {
this.problem1 = new Problem($(".xblock-student_view"));
expect(this.problem1.element_id).toBe("problem_1");
});
});
describe("bind", function () {
beforeEach(function () {
spyOn(window, "update_schematics");
MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
this.problem = new Problem($(".xblock-student_view"));
});
it("set mathjax typeset", () => expect(MathJax.Hub.Queue).toHaveBeenCalled());
it("update schematics", () => expect(window.update_schematics).toHaveBeenCalled());
it("bind answer refresh on button click", function () {
expect($("div.action button")).toHandleWith("click", this.problem.refreshAnswers);
});
it("bind the submit button", function () {
expect($(".action .submit")).toHandleWith("click", this.problem.submit_fd);
});
it("bind the reset button", function () {
expect($("div.action button.reset")).toHandleWith("click", this.problem.reset);
});
it("bind the show button", function () {
expect($(".action .show")).toHandleWith("click", this.problem.show);
});
it("bind the save button", function () {
expect($("div.action button.save")).toHandleWith("click", this.problem.save);
});
it("bind the math input", function () {
expect($("input.math")).toHandleWith("keyup", this.problem.refreshMath);
});
});
describe("bind_with_custom_input_id", function () {
beforeEach(function () {
spyOn(window, "update_schematics");
MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
this.problem = new Problem($(".xblock-student_view"));
return $(this).html(readFixtures("problem_content_1240.html"));
});
it("bind the submit button", function () {
expect($(".action .submit")).toHandleWith("click", this.problem.submit_fd);
});
it("bind the show button", function () {
expect($("div.action button.show")).toHandleWith("click", this.problem.show);
});
});
describe("renderProgressState", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
});
const testProgessData = function (
problem,
score,
total_possible,
attempts,
graded,
expected_progress_after_render,
) {
problem.el.data("problem-score", score);
problem.el.data("problem-total-possible", total_possible);
problem.el.data("attempts-used", attempts);
problem.el.data("graded", graded);
expect(problem.$(".problem-progress").html()).toEqual("");
problem.renderProgressState();
expect(problem.$(".problem-progress").html()).toEqual(expected_progress_after_render);
};
describe('with a status of "none"', function () {
it("reports the number of points possible and graded", function () {
testProgessData(this.problem, 0, 1, 0, "True", "1 point possible (graded)");
});
it("displays the number of points possible when rendering happens with the content", function () {
testProgessData(this.problem, 0, 2, 0, "True", "2 points possible (graded)");
});
it("reports the number of points possible and ungraded", function () {
testProgessData(this.problem, 0, 1, 0, "False", "1 point possible (ungraded)");
});
it("displays ungraded if number of points possible is 0", function () {
testProgessData(this.problem, 0, 0, 0, "False", "0 points possible (ungraded)");
});
it("displays ungraded if number of points possible is 0, even if graded value is True", function () {
testProgessData(this.problem, 0, 0, 0, "True", "0 points possible (ungraded)");
});
it("reports the correct score with status none and >0 attempts", function () {
testProgessData(this.problem, 0, 1, 1, "True", "0/1 point (graded)");
});
it("reports the correct score with >1 weight, status none, and >0 attempts", function () {
testProgessData(this.problem, 0, 2, 2, "True", "0/2 points (graded)");
});
});
describe("with any other valid status", function () {
it("reports the current score", function () {
testProgessData(this.problem, 1, 1, 1, "True", "1/1 point (graded)");
});
it("shows current score when rendering happens with the content", function () {
testProgessData(this.problem, 2, 2, 1, "True", "2/2 points (graded)");
});
it("reports the current score even if problem is ungraded", function () {
testProgessData(this.problem, 1, 1, 1, "False", "1/1 point (ungraded)");
});
});
describe('with valid status and string containing an integer like "0" for detail', () =>
// These tests are to address a failure specific to Chrome 51 and 52 +
it("shows 0 points possible for the detail", function () {
testProgessData(this.problem, 0, 0, 1, "False", "0 points possible (ungraded)");
}));
describe("with a score of null (show_correctness == false)", function () {
it("reports the number of points possible and graded, results hidden", function () {
testProgessData(this.problem, null, 1, 0, "True", "1 point possible (graded, results hidden)");
});
it("reports the number of points possible (plural) and graded, results hidden", function () {
testProgessData(this.problem, null, 2, 0, "True", "2 points possible (graded, results hidden)");
});
it("reports the number of points possible and ungraded, results hidden", function () {
testProgessData(this.problem, null, 1, 0, "False", "1 point possible (ungraded, results hidden)");
});
it("displays ungraded if number of points possible is 0, results hidden", function () {
testProgessData(this.problem, null, 0, 0, "False", "0 points possible (ungraded, results hidden)");
});
it("displays ungraded if number of points possible is 0, even if graded value is True, results hidden", function () {
testProgessData(this.problem, null, 0, 0, "True", "0 points possible (ungraded, results hidden)");
});
it("reports the correct score with status none and >0 attempts, results hidden", function () {
testProgessData(this.problem, null, 1, 1, "True", "1 point possible (graded, results hidden)");
});
it("reports the correct score with >1 weight, status none, and >0 attempts, results hidden", function () {
testProgessData(this.problem, null, 2, 2, "True", "2 points possible (graded, results hidden)");
});
});
});
describe("render", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.bind = this.problem.bind;
spyOn(this.problem, "bind");
});
describe("with content given", function () {
beforeEach(function () {
this.problem.render("Hello World");
});
it("render the content", function () {
expect(this.problem.el.html()).toEqual("Hello World");
});
it("re-bind the content", function () {
expect(this.problem.bind).toHaveBeenCalled();
});
});
describe("with no content given", function () {
beforeEach(function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ html: "Hello World" }));
this.problem.render();
});
it("load the content via ajax", function () {
expect(this.problem.el.html()).toEqual("Hello World");
});
it("re-bind the content", function () {
expect(this.problem.bind).toHaveBeenCalled();
});
});
});
describe("submit_fd", function () {
beforeEach(function () {
// Insert an input of type file outside of the problem.
$(".xblock-student_view").after('<input type="file" />');
this.problem = new Problem($(".xblock-student_view"));
spyOn(this.problem, "submit");
});
it("submit method is called if input of type file is not in problem", function () {
this.problem.submit_fd();
expect(this.problem.submit).toHaveBeenCalled();
});
});
describe("submit", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.problem.answers = "foo=1&bar=2";
});
it("log the problem_check event", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
promise = {
always(callable) {
return callable();
},
done(callable) {
return callable();
},
};
return promise;
});
this.problem.submit();
expect(Logger.log).toHaveBeenCalledWith("problem_check", "foo=1&bar=2");
});
it("log the problem_graded event, after the problem is done grading.", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
const response = {
success: "correct",
contents: "mock grader response",
};
callback(response);
promise = {
always(callable) {
return callable();
},
done(callable) {
return callable();
},
};
return promise;
});
this.problem.submit();
expect(Logger.log).toHaveBeenCalledWith(
"problem_graded",
["foo=1&bar=2", "mock grader response"],
this.problem.id,
);
});
it("submit the answer for submit", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
promise = {
always(callable) {
return callable();
},
done(callable) {
return callable();
},
};
return promise;
});
this.problem.submit();
expect($.postWithPrefix).toHaveBeenCalledWith(
"/problem/Problem1/problem_check",
"foo=1&bar=2",
jasmine.any(Function),
);
});
describe("when the response is correct", () =>
it("call render with returned content", function () {
const contents =
'<div class="wrapper-problem-response" aria-label="Question 1"><p>Correct<span class="status">excellent</span></p></div>' +
'<div class="wrapper-problem-response" aria-label="Question 2"><p>Yep<span class="status">correct</span></p></div>';
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
callback({ success: "correct", contents });
promise = {
always(callable) {
return callable();
},
done(callable) {
return callable();
},
};
return promise;
});
this.problem.submit();
expect(this.problem.el).toHaveHtml(contents);
expect(window.SR.readTexts).toHaveBeenCalledWith(["Question 1: excellent", "Question 2: correct"]);
}));
describe("when the response is incorrect", () =>
it("call render with returned content", function () {
const contents = '<p>Incorrect<span class="status">no, try again</span></p>';
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
callback({ success: "incorrect", contents });
promise = {
always(callable) {
return callable();
},
done(callable) {
return callable();
},
};
return promise;
});
this.problem.submit();
expect(this.problem.el).toHaveHtml(contents);
expect(window.SR.readTexts).toHaveBeenCalledWith(["no, try again"]);
}));
it("tests if the submit button is disabled while submitting and the text changes on the button", function () {
const self = this;
const curr_html = this.problem.el.html();
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
// At this point enableButtons should have been called, making the submit button disabled with text 'submitting'
let promise;
expect(self.problem.submitButton).toHaveAttr("disabled");
expect(self.problem.submitButtonLabel.text()).toBe("Submitting");
callback({
success: "incorrect", // does not matter if correct or incorrect here
contents: curr_html,
});
promise = {
always(callable) {
return callable();
},
done(callable) {
return callable();
},
};
return promise;
});
// Make sure the submit button is enabled before submitting
$("#input_example_1").val("test").trigger("input");
expect(this.problem.submitButton).not.toHaveAttr("disabled");
this.problem.submit();
// After submit, the button should not be disabled and should have text as 'Submit'
expect(this.problem.submitButtonLabel.text()).toBe("Submit");
expect(this.problem.submitButton).not.toHaveAttr("disabled");
});
});
describe("submit button on problems", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.submitDisabled = (disabled) => {
if (disabled) {
expect(this.problem.submitButton).toHaveAttr("disabled");
} else {
expect(this.problem.submitButton).not.toHaveAttr("disabled");
}
};
});
describe("some basic tests for submit button", () =>
it("should become enabled after a value is entered into the text box", function () {
$("#input_example_1").val("test").trigger("input");
this.submitDisabled(false);
$("#input_example_1").val("").trigger("input");
this.submitDisabled(true);
}));
describe("some advanced tests for submit button", function () {
const radioButtonProblemHtml = readFixtures("radiobutton_problem.html");
const checkboxProblemHtml = readFixtures("checkbox_problem.html");
it("should become enabled after a checkbox is checked", function () {
$("#input_example_1").replaceWith(checkboxProblemHtml);
this.problem.submitAnswersAndSubmitButton(true);
this.submitDisabled(true);
$("#input_1_1_1").click();
this.submitDisabled(false);
$("#input_1_1_1").click();
this.submitDisabled(true);
});
it("should become enabled after a radiobutton is checked", function () {
$("#input_example_1").replaceWith(radioButtonProblemHtml);
this.problem.submitAnswersAndSubmitButton(true);
this.submitDisabled(true);
$("#input_1_1_1").attr("checked", true).trigger("click");
this.submitDisabled(false);
$("#input_1_1_1").attr("checked", false).trigger("click");
this.submitDisabled(true);
});
it("should become enabled after a value is selected in a selector", function () {
const html = `\
<div id="problem_sel">
<select>
<option value="val0">Select an option</option>
<option value="val1">1</option>
<option value="val2">2</option>
</select>
</div>\
`;
$("#input_example_1").replaceWith(html);
this.problem.submitAnswersAndSubmitButton(true);
this.submitDisabled(true);
$("#problem_sel select").val("val2").trigger("change");
this.submitDisabled(false);
$("#problem_sel select").val("val0").trigger("change");
this.submitDisabled(true);
});
it("should become enabled after a radiobutton is checked and a value is entered into the text box", function () {
$(radioButtonProblemHtml).insertAfter("#input_example_1");
this.problem.submitAnswersAndSubmitButton(true);
this.submitDisabled(true);
$("#input_1_1_1").attr("checked", true).trigger("click");
this.submitDisabled(true);
$("#input_example_1").val("111").trigger("input");
this.submitDisabled(false);
$("#input_1_1_1").attr("checked", false).trigger("click");
this.submitDisabled(true);
});
it("should become enabled if there are only hidden input fields", function () {
const html = `\
<input type="text" name="test" id="test" aria-describedby="answer_test" value="" style="display:none;">\
`;
$("#input_example_1").replaceWith(html);
this.problem.submitAnswersAndSubmitButton(true);
this.submitDisabled(false);
});
});
});
describe("reset", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
});
it("log the problem_reset event", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
promise = {
always(callable) {
return callable();
},
};
return promise;
});
this.problem.answers = "foo=1&bar=2";
this.problem.reset();
expect(Logger.log).toHaveBeenCalledWith("problem_reset", "foo=1&bar=2");
});
it("POST to the problem reset page", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
promise = {
always(callable) {
return callable();
},
};
return promise;
});
this.problem.reset();
expect($.postWithPrefix).toHaveBeenCalledWith(
"/problem/Problem1/problem_reset",
{ id: "i4x://edX/101/problem/Problem1" },
jasmine.any(Function),
);
});
it("render the returned content", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
callback({ html: "Reset", success: true });
promise = {
always(callable) {
return callable();
},
};
return promise;
});
this.problem.reset();
expect(this.problem.el.html()).toEqual("Reset");
});
it("sends a message to the window SR element", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
callback({ html: "Reset", success: true });
promise = {
always(callable) {
return callable();
},
};
return promise;
});
this.problem.reset();
expect(window.SR.readText).toHaveBeenCalledWith("This problem has been reset.");
});
it("shows a notification on error", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
callback({ msg: "Error on reset.", success: false });
promise = {
always(callable) {
return callable();
},
};
return promise;
});
this.problem.reset();
expect($(".notification-gentle-alert .notification-message").text()).toEqual("Error on reset.");
});
it("tests that reset does not enable submit or modify the text while resetting", function () {
const self = this;
const curr_html = this.problem.el.html();
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
// enableButtons should have been called at this point to set them to all disabled
let promise;
expect(self.problem.submitButton).toHaveAttr("disabled");
expect(self.problem.submitButtonLabel.text()).toBe("Submit");
callback({ success: "correct", html: curr_html });
promise = {
always(callable) {
return callable();
},
};
return promise;
});
// Submit should be disabled
expect(this.problem.submitButton).toHaveAttr("disabled");
this.problem.reset();
// Submit should remain disabled
expect(self.problem.submitButton).toHaveAttr("disabled");
expect(self.problem.submitButtonLabel.text()).toBe("Submit");
});
});
describe("show problem with column in id", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.problem.el.prepend('<div id="answer_1_1:11" /><div id="answer_1_2:12" />');
});
it("log the problem_show event", function () {
this.problem.show();
expect(Logger.log).toHaveBeenCalledWith("problem_show", { problem: "i4x://edX/101/problem/Problem1" });
});
it("fetch the answers", function () {
spyOn($, "postWithPrefix");
this.problem.show();
expect($.postWithPrefix).toHaveBeenCalledWith("/problem/Problem1/problem_show", jasmine.any(Function));
});
it("show the answers", function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) =>
callback({ answers: { "1_1:11": "One", "1_2:12": "Two" } }),
);
this.problem.show();
expect($("#answer_1_1\\:11")).toHaveHtml("One");
expect($("#answer_1_2\\:12")).toHaveHtml("Two");
});
it("disables the show answer button", function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ answers: {} }));
this.problem.show();
expect(this.problem.el.find(".show").attr("disabled")).toEqual("disabled");
});
});
describe("show", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.problem.el.prepend('<div id="answer_1_1" /><div id="answer_1_2" />');
});
describe("when the answer has not yet shown", function () {
beforeEach(function () {
expect(this.problem.el.find(".show").attr("disabled")).not.toEqual("disabled");
});
it("log the problem_show event", function () {
this.problem.show();
expect(Logger.log).toHaveBeenCalledWith("problem_show", { problem: "i4x://edX/101/problem/Problem1" });
});
it("fetch the answers", function () {
spyOn($, "postWithPrefix");
this.problem.show();
expect($.postWithPrefix).toHaveBeenCalledWith("/problem/Problem1/problem_show", jasmine.any(Function));
});
it("show the answers", function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) =>
callback({ answers: { "1_1": "One", "1_2": "Two" } }),
);
this.problem.show();
expect($("#answer_1_1")).toHaveHtml("One");
expect($("#answer_1_2")).toHaveHtml("Two");
});
it("disables the show answer button", function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ answers: {} }));
this.problem.show();
expect(this.problem.el.find(".show").attr("disabled")).toEqual("disabled");
});
describe("radio text question", function () {
const radio_text_xml = `\
<section class="problem">
<div><p></p><span><section id="choicetextinput_1_2_1" class="choicetextinput">
<form class="choicetextgroup capa_inputtype" id="inputtype_1_2_1">
<div class="indicator-container">
<span class="unanswered" style="display:inline-block;" id="status_1_2_1"></span>
</div>
<fieldset>
<section id="forinput1_2_1_choiceinput_0bc">
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_0bc" value="choiceinput_0"">
<input class="ctinput" type="text" name="choiceinput_0_textinput_0" id="1_2_1_choiceinput_0_textinput_0" value=" ">
<p id="answer_1_2_1_choiceinput_0bc" class="answer"></p>
</>
<section id="forinput1_2_1_choiceinput_1bc">
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_1bc" value="choiceinput_1" >
<input class="ctinput" type="text" name="choiceinput_1_textinput_0" id="1_2_1_choiceinput_1_textinput_0" value=" " >
<p id="answer_1_2_1_choiceinput_1bc" class="answer"></p>
</section>
<section id="forinput1_2_1_choiceinput_2bc">
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_2bc" value="choiceinput_2" >
<input class="ctinput" type="text" name="choiceinput_2_textinput_0" id="1_2_1_choiceinput_2_textinput_0" value=" " >
<p id="answer_1_2_1_choiceinput_2bc" class="answer"></p>
</section></fieldset><input class="choicetextvalue" type="hidden" name="input_1_2_1" id="input_1_2_1"></form>
</section></span></div>
</section>\
`;
beforeEach(function () {
// Append a radiotextresponse problem to the problem, so we can check it's javascript functionality
this.problem.el.prepend(radio_text_xml);
});
it("sets the correct class on the section for the correct choice", function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) =>
callback({ answers: { "1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3" } }),
);
this.problem.show();
expect($("#forinput1_2_1_choiceinput_0bc").attr("class")).toEqual("choicetextgroup_show_correct");
expect($("#answer_1_2_1_choiceinput_0bc").text()).toEqual("3");
expect($("#answer_1_2_1_choiceinput_1bc").text()).toEqual("");
expect($("#answer_1_2_1_choiceinput_2bc").text()).toEqual("");
});
it("Should not disable input fields", function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) =>
callback({ answers: { "1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3" } }),
);
this.problem.show();
expect($("input#1_2_1_choiceinput_0bc").attr("disabled")).not.toEqual("disabled");
expect($("input#1_2_1_choiceinput_1bc").attr("disabled")).not.toEqual("disabled");
expect($("input#1_2_1_choiceinput_2bc").attr("disabled")).not.toEqual("disabled");
expect($("input#1_2_1").attr("disabled")).not.toEqual("disabled");
});
});
describe("imageinput", function () {
let el, height, width;
const imageinput_html = readFixtures("imageinput.underscore");
const DEFAULTS = {
id: "12345",
width: "300",
height: "400",
};
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.problem.el.prepend(_.template(imageinput_html)(DEFAULTS));
});
const assertAnswer = (problem, data) => {
stubRequest(data);
problem.show();
$.each(data["answers"], (id, answer) => {
const img = getImage(answer);
el = $(`#inputtype_${id}`);
expect(img).toImageDiffEqual(el.find("canvas")[0]);
});
};
var stubRequest = (data) => {
spyOn($, "postWithPrefix").and.callFake((url, callback) => callback(data));
};
var getImage = (coords, c_width, c_height) => {
let ctx, reg;
const types = {
rectangle: (coords) => {
reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/;
const rects = coords.replace(/\s*/g, "").split(/;/);
$.each(rects, (index, rect) => {
const { abs } = Math;
const points = reg.exec(rect);
if (points) {
width = abs(points[3] - points[1]);
height = abs(points[4] - points[2]);
return ctx.rect(points[1], points[2], width, height);
}
});
ctx.stroke();
ctx.fill();
},
regions: (coords) => {
const parseCoords = (coords) => {
reg = JSON.parse(coords);
if (typeof reg[0][0][0] === "undefined") {
reg = [reg];
}
return reg;
};
return $.each(parseCoords(coords), (index, region) => {
ctx.beginPath();
$.each(region, (index, point) => {
if (index === 0) {
return ctx.moveTo(point[0], point[1]);
} else {
return ctx.lineTo(point[0], point[1]);
}
});
ctx.closePath();
ctx.stroke();
ctx.fill();
});
},
};
const canvas = document.createElement("canvas");
canvas.width = c_width || 100;
canvas.height = c_height || 100;
if (canvas.getContext) {
ctx = canvas.getContext("2d");
} else {
console.log("Canvas is not supported.");
}
ctx.fillStyle = "rgba(255,255,255,.3)";
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = "2";
$.each(coords, (key, value) => {
if (types[key] != null && value) {
return types[key](value);
}
});
return canvas;
};
it("rectangle is drawn correctly", function () {
assertAnswer(this.problem, {
answers: {
12345: {
rectangle: "(10,10)-(30,30)",
regions: null,
},
},
});
});
it("region is drawn correctly", function () {
assertAnswer(this.problem, {
answers: {
12345: {
rectangle: null,
regions: "[[10,10],[30,30],[70,30],[20,30]]",
},
},
});
});
it("mixed shapes are drawn correctly", function () {
assertAnswer(this.problem, {
answers: {
12345: {
rectangle: "(10,10)-(30,30);(5,5)-(20,20)",
regions: `[
[[50,50],[40,40],[70,30],[50,70]],
[[90,95],[95,95],[90,70],[70,70]]
]`,
},
},
});
});
it("multiple image inputs draw answers on separate canvases", function () {
const data = {
id: "67890",
width: "400",
height: "300",
};
this.problem.el.prepend(_.template(imageinput_html)(data));
assertAnswer(this.problem, {
answers: {
12345: {
rectangle: null,
regions: "[[10,10],[30,30],[70,30],[20,30]]",
},
67890: {
rectangle: "(10,10)-(30,30)",
regions: null,
},
},
});
});
it("dictionary with answers doesn't contain answer for current id", function () {
spyOn(console, "log");
stubRequest({ answers: {} });
this.problem.show();
el = $("#inputtype_12345");
expect(el.find("canvas")).not.toExist();
expect(console.log).toHaveBeenCalledWith("Answer is absent for image input with id=12345");
});
});
});
});
describe("save", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.problem.answers = "foo=1&bar=2";
});
it("log the problem_save event", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
promise = {
always(callable) {
return callable();
},
};
return promise;
});
this.problem.save();
expect(Logger.log).toHaveBeenCalledWith("problem_save", "foo=1&bar=2");
});
it("POST to save problem", function () {
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
let promise;
promise = {
always(callable) {
return callable();
},
};
return promise;
});
this.problem.save();
expect($.postWithPrefix).toHaveBeenCalledWith(
"/problem/Problem1/problem_save",
"foo=1&bar=2",
jasmine.any(Function),
);
});
it("tests that save does not enable the submit button or change the text when submit is originally disabled", function () {
const self = this;
const curr_html = this.problem.el.html();
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
// enableButtons should have been called at this point and the submit button should be unaffected
let promise;
expect(self.problem.submitButton).toHaveAttr("disabled");
expect(self.problem.submitButtonLabel.text()).toBe("Submit");
callback({ success: "correct", html: curr_html });
promise = {
always(callable) {
return callable();
},
};
return promise;
});
// Expect submit to be disabled and labeled properly at the start
expect(this.problem.submitButton).toHaveAttr("disabled");
expect(this.problem.submitButtonLabel.text()).toBe("Submit");
this.problem.save();
// Submit button should have the same state after save has completed
expect(this.problem.submitButton).toHaveAttr("disabled");
expect(this.problem.submitButtonLabel.text()).toBe("Submit");
});
it("tests that save does not disable the submit button or change the text when submit is originally enabled", function () {
const self = this;
const curr_html = this.problem.el.html();
spyOn($, "postWithPrefix").and.callFake(function (url, answers, callback) {
// enableButtons should have been called at this point, and the submit button should be disabled while submitting
let promise;
expect(self.problem.submitButton).toHaveAttr("disabled");
expect(self.problem.submitButtonLabel.text()).toBe("Submit");
callback({ success: "correct", html: curr_html });
promise = {
always(callable) {
return callable();
},
};
return promise;
});
// Expect submit to be enabled and labeled properly at the start after adding an input
$("#input_example_1").val("test").trigger("input");
expect(this.problem.submitButton).not.toHaveAttr("disabled");
expect(this.problem.submitButtonLabel.text()).toBe("Submit");
this.problem.save();
// Submit button should have the same state after save has completed
expect(this.problem.submitButton).not.toHaveAttr("disabled");
expect(this.problem.submitButtonLabel.text()).toBe("Submit");
});
});
describe("refreshMath", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
$("#input_example_1").val("E=mc^2");
this.problem.refreshMath({ target: $("#input_example_1").get(0) });
});
it("should queue the conversion and MathML element update", function () {
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
["Text", this.stubbedJax, "E=mc^2"],
[this.problem.updateMathML, this.stubbedJax, $("#input_example_1").get(0)],
);
});
});
describe("updateMathML", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.stubbedJax.root.toMathML.and.returnValue("<MathML>");
});
describe("when there is no exception", function () {
beforeEach(function () {
this.problem.updateMathML(this.stubbedJax, $("#input_example_1").get(0));
});
it("convert jax to MathML", () => expect($("#input_example_1_dynamath")).toHaveValue("<MathML>"));
});
describe("when there is an exception", function () {
beforeEach(function () {
const error = new Error();
error.restart = true;
this.stubbedJax.root.toMathML.and.throwError(error);
this.problem.updateMathML(this.stubbedJax, $("#input_example_1").get(0));
});
it("should queue up the exception", function () {
expect(MathJax.Callback.After).toHaveBeenCalledWith([this.problem.refreshMath, this.stubbedJax], true);
});
});
});
describe("refreshAnswers", function () {
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.problem.el.html(`\
<textarea class="CodeMirror" />
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
<input id="input_1_2" name="input_1_2" value="two" />
<input id="input_bogus_3" name="input_bogus_3" value="three" />\
`);
this.stubSchematic = { update_value: jasmine.createSpy("schematic") };
this.stubCodeMirror = { save: jasmine.createSpy("CodeMirror") };
$("input.schematic").get(0).schematic = this.stubSchematic;
$("textarea.CodeMirror").get(0).CodeMirror = this.stubCodeMirror;
});
it("update each schematic", function () {
this.problem.refreshAnswers();
expect(this.stubSchematic.update_value).toHaveBeenCalled();
});
it("update each code block", function () {
this.problem.refreshAnswers();
expect(this.stubCodeMirror.save).toHaveBeenCalled();
});
});
describe("multiple JsInput in single problem", function () {
const jsinput_html = readFixtures("jsinput_problem.html");
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.problem.render(jsinput_html);
});
it("submit_save_waitfor should return false", function () {
$(this.problem.inputs[0]).data("waitfor", function () {});
expect(this.problem.submit_save_waitfor()).toEqual(false);
});
});
describe("Submitting an xqueue-graded problem", function () {
const matlabinput_html = readFixtures("matlabinput_problem.html");
beforeEach(function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ html: matlabinput_html }));
jasmine.clock().install();
this.problem = new Problem($(".xblock-student_view"));
spyOn(this.problem, "poll").and.callThrough();
this.problem.render(matlabinput_html);
});
afterEach(() => jasmine.clock().uninstall());
it("check that we stop polling after a fixed amount of time", function () {
expect(this.problem.poll).not.toHaveBeenCalled();
jasmine.clock().tick(1);
const time_steps = [1000, 2000, 4000, 8000, 16000, 32000];
let num_calls = 1;
for (let time_step of Array.from(time_steps)) {
((time_step) => {
jasmine.clock().tick(time_step);
expect(this.problem.poll.calls.count()).toEqual(num_calls);
num_calls += 1;
})(time_step);
}
// jump the next step and verify that we are not still continuing to poll
jasmine.clock().tick(64000);
expect(this.problem.poll.calls.count()).toEqual(6);
expect($(".notification-gentle-alert .notification-message").text()).toEqual(
"The grading process is still running. Refresh the page to see updates.",
);
});
});
describe("codeinput problem", function () {
const codeinputProblemHtml = readFixtures("codeinput_problem.html");
beforeEach(function () {
spyOn($, "postWithPrefix").and.callFake((url, callback) => callback({ html: codeinputProblemHtml }));
this.problem = new Problem($(".xblock-student_view"));
this.problem.render(codeinputProblemHtml);
});
it("has rendered with correct a11y info", function () {
const CodeMirrorTextArea = $("textarea")[1];
const CodeMirrorTextAreaId = "cm-textarea-101";
// verify that question label has correct `for` attribute value
expect($(".problem-group-label").attr("for")).toEqual(CodeMirrorTextAreaId);
// verify that codemirror textarea has correct `id` attribute value
expect($(CodeMirrorTextArea).attr("id")).toEqual(CodeMirrorTextAreaId);
// verify that codemirror textarea has correct `aria-describedby` attribute value
expect($(CodeMirrorTextArea).attr("aria-describedby")).toEqual("cm-editor-exit-message-101 status_101");
});
});
describe("show answer button", function () {
const radioButtonProblemHtml = readFixtures("radiobutton_problem.html");
const checkboxProblemHtml = readFixtures("checkbox_problem.html");
beforeEach(function () {
this.problem = new Problem($(".xblock-student_view"));
this.checkAssertionsAfterClickingAnotherOption = () => {
// verify that 'show answer button is no longer disabled'
expect(this.problem.el.find(".show").attr("disabled")).not.toEqual("disabled");
// verify that displayed answer disappears
expect(this.problem.el.find("div.choicegroup")).not.toHaveClass("choicegroup_correct");
// verify that radio/checkbox label has no span having class '.status.correct'
expect(this.problem.el.find("div.choicegroup")).not.toHaveAttr("span.status.correct");
};
});
it("should become enabled after a radiobutton is selected", function () {
$("#input_example_1").replaceWith(radioButtonProblemHtml);
// assume that 'ShowAnswer' button is clicked,
// clicking make it disabled.
this.problem.el.find(".show").attr("disabled", "disabled");
// bind click event to input fields
this.problem.submitAnswersAndSubmitButton(true);
// selects option 2
$("#input_1_1_2").attr("checked", true).trigger("click");
this.checkAssertionsAfterClickingAnotherOption();
});
it("should become enabled after a checkbox is selected", function () {
$("#input_example_1").replaceWith(checkboxProblemHtml);
this.problem.el.find(".show").attr("disabled", "disabled");
this.problem.submitAnswersAndSubmitButton(true);
$("#input_1_1_2").click();
this.checkAssertionsAfterClickingAnotherOption();
});
});
});