canvas-lms/spec/helpers/quizzes_helper_spec.rb

566 lines
21 KiB
Ruby

# frozen_string_literal: true
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
describe QuizzesHelper do
include ApplicationHelper
include QuizzesHelper
include ERB::Util
describe "#needs_unpublished_warning" do
before :once do
course_with_teacher
end
it "is false if quiz not manageable" do
quiz = Quizzes::Quiz.new(context: @course)
allow(self).to receive(:can_publish).and_return(false)
expect(needs_unpublished_warning?(quiz)).to be_falsey
end
it "is false if quiz is available with no unpublished changes" do
quiz = Quizzes::Quiz.new(context: @course)
quiz.workflow_state = "available"
quiz.last_edited_at = 10.minutes.ago
quiz.published_at = Time.now
allow(self).to receive(:can_publish).and_return(true)
expect(needs_unpublished_warning?(quiz)).to be_falsey
end
it "is true if quiz is not available" do
quiz = Quizzes::Quiz.new(context: @course)
quiz.workflow_state = "created"
allow(self).to receive(:can_publish).and_return(true)
expect(needs_unpublished_warning?(quiz)).to be_truthy
end
it "is true if quiz has unpublished changes" do
quiz = Quizzes::Quiz.new(context: @course)
quiz.workflow_state = "available"
quiz.last_edited_at = Time.now
quiz.published_at = 10.minutes.ago
allow(self).to receive(:can_publish).and_return(true)
expect(needs_unpublished_warning?(quiz)).to be_truthy
end
end
describe "#attachment_id_for" do
it "returns the attachment id if attachment exists" do
question = { id: 1 }
@attachments = { 1 => { id: "11" } }
@stored_params = { "question_1" => ["1"] }
expect(attachment_id_for(question)).to eq "11"
end
it "returns empty string when no attachments stored" do
question = { id: 1 }
@stored_params = {}
@attachments = {}
expect(attachment_id_for(question)).to eq nil
end
end
context "render_number" do
it "renders numbers" do
expect(render_number(1)).to eq "1"
expect(render_number(100)).to eq "100"
expect(render_number(1.123)).to eq "1.123"
expect(render_number(1000.45)).to eq "1,000.45"
expect(render_number(1000.45966)).to eq "1,000.45966"
expect(render_number("100")).to eq "100"
expect(render_number("1.43")).to eq "1.43"
end
it "removes trailing zeros" do
expect(render_number(1.20000000)).to eq "1.2"
expect(render_number(0.10340000)).to eq "0.1034"
end
it "removes trailing zeros and decimal point" do
expect(render_number(0.00000000)).to eq "0"
expect(render_number(1.00000000)).to eq "1"
expect(render_number(100.0)).to eq "100"
end
it "renders percentages" do
expect(render_number("1234.456%")).to eq "1,234.456%"
end
end
context "render_score" do
it "renders nil scores" do
expect(render_score(nil)).to eq "_"
end
it "renders with default precision" do
expect(render_score(1000.45966)).to eq "1,000.46"
expect(render_score("12.3456")).to eq "12.35"
end
it "supports higher precision" do
expect(render_score(1234.4567, 3)).to eq "1,234.457"
expect(render_score(0.12345000, 4)).to eq "0.1235"
end
end
context "render_quiz_type" do
it "renders a humanized quiz type string" do
expect(render_quiz_type("practice_quiz")).to eq "Practice Quiz"
expect(render_quiz_type("assignment")).to eq "Graded Quiz"
expect(render_quiz_type("graded_survey")).to eq "Graded Survey"
expect(render_quiz_type("survey")).to eq "Ungraded Survey"
end
it "returns nil for an unrecognized quiz_type" do
expect(render_quiz_type(nil)).to be_nil
expect(render_quiz_type("made_up_quiz_type")).to be_nil
end
end
context "render_score_to_keep" do
it "renders which score to keep when passed in a scoring_policy option" do
expect(render_score_to_keep("keep_highest")).to eq "Highest"
expect(render_score_to_keep("keep_latest")).to eq "Latest"
end
it "returns nil for an unrecognized scoring_policy" do
expect(render_score_to_keep(nil)).to be_nil
expect(render_score_to_keep("made_up_scoring_policy")).to be_nil
end
end
context "render_show_responses" do
it "answers 'Let Students See Quiz Responses?' when passed a hide_results option" do
expect(render_show_responses("always")).to eq "No"
expect(render_show_responses("until_after_last_attempt")).to eq "After Last Attempt"
expect(render_show_responses(nil)).to eq "Always"
end
it "returns nil for an unrecognized hide_results value" do
expect(render_show_responses("made_up_hide_results")).to be_nil
end
end
context "score_out_of_points_possible" do
it "shows single digit scores" do
expect(score_out_of_points_possible(1, 5)).to eq "1 out of 5"
expect(score_out_of_points_possible(0, 9)).to eq "0 out of 9"
end
it "shows 2-decimal precision if necessary" do
expect(score_out_of_points_possible(0.66666666666, 1)).to eq "0.67 out of 1"
expect(score_out_of_points_possible(5.23333333333, 10.0)).to eq "5.23 out of 10"
end
it "is wrapped by a span when a CSS class, id, or style is given" do
expect(score_out_of_points_possible(1.5, 3, class: "score_value")).to eq( \
'<span class="score_value">1.5</span> out of 3'
)
expect(score_out_of_points_possible(1.5, 3, id: "score")).to eq( \
'<span id="score">1.5</span> out of 3'
)
expect(score_out_of_points_possible(1.5, 3, style: "width:100%")).to eq( \
'<span style="width:100%">1.5</span> out of 3'
)
end
end
context "fill_in_multiple_blanks_question" do
before do
@question_text = %(<input name="question_1_1813d2a7223184cf43e19db6622df40b" 'value={{question_1}}' />)
@answer_list = []
@answers = []
# double #user_content
def user_content(stuff)
stuff
end
end
it "extracts the answers by blank" do
@answer_list = [{ blank_id: "color", answer: "red" }]
html = fill_in_multiple_blanks_question(
question: { question_text: @question_text },
answer_list: @answer_list,
answers: @answers
)
expect(html).to eq %(<input name="question_1_1813d2a7223184cf43e19db6622df40b" 'value=red' readonly="readonly" aria-label='Fill in the blank, read surrounding text' />)
end
it "sanitizes user input" do
malicious_answer_list = [{
blank_id: "color",
answer: "><script>alert()</script><img"
}]
html = fill_in_multiple_blanks_question(
question: { question_text: @question_text },
answer_list: malicious_answer_list,
answers: @answers
)
expect(html).to eq %|<input name="question_1_1813d2a7223184cf43e19db6622df40b" 'value=&gt;&lt;script&gt;alert()&lt;/script&gt;&lt;img' readonly="readonly" aria-label='Fill in the blank, read surrounding text' />|
expect(html).to be_html_safe
end
it "adds an appropriate label" do
html = fill_in_multiple_blanks_question(
question: { question_text: @question_text },
answer_list: @answer_list,
answers: @answers
)
expect(html).to match(/aria-label/)
expect(html).to match(/Fill in the blank/)
end
it "handles equation img tags in the question text" do
broken_question_text = "\"<p>Rubisco is a <input class='question_input' type='text' autocomplete='off' style='width: 120px;' name=\\\"question_8_26534e6c8737f63335d5d98ca4136d09\\\" value='{{question_8_26534e6c8737f63335d5d98ca4136d09}}' > responsible for the first enzymatic step of carbon <input class='question_input' type='text' autocomplete='off' style='width: 120px;' name='question_8_f8e302199c03689d87c52e942b56e1f4' value='{{question_8_f8e302199c03689d87c52e942b56e1f4}}' >. <br><br>equation here: <img class=\\\"equation_image\\\" title=\\\"\\sum\\frac{k}{l}\\\" src=\\\"/equation_images/%255Csum%255Cfrac%257Bk%257D%257Bl%257D\\\" alt=\\\"\\sum\\frac{k}{l}\\\"></p>\""
@answer_list = [
{ blank_id: "kindof", answer: "protein" },
{ blank_id: "role", answer: "fixing" }
]
html = fill_in_multiple_blanks_question(
question: { question_text: broken_question_text },
answer_list: @answer_list,
answers: @answers
)
expect(html).to match(/"readonly"/)
expect(html).to match(/value='fixing'/)
expect(html).to match(/value='protein'/)
end
it "sanitizes the answer blocks in the noisy question data" do
broken_question_text = "<p><span>\"Roses are <input\n class='question_input'\n type='text'\n autocomplete='off'\n style='width: 120px;'\n name='question_244_ec9a1c7e5a9f3a6278e9055d8dec00f0'\n value='{{question_244_ec9a1c7e5a9f3a6278e9055d8dec00f0}}' />\n, violets are <input\n class='question_input'\n type='text'\n autocomplete='off'\n style='width: 120px;'\n name='question_244_01731fa53c4cf2f32e893d5c3dbae9c1'\n value='{{question_244_01731fa53c4cf2f32e893d5c3dbae9c1}}' />\n\")</span></p>"
html = fill_in_multiple_blanks_question(
question: { question_text: ActiveSupport::SafeBuffer.new(broken_question_text) },
answer_list: [
{ blank_id: "color1", answer: "red" },
{ blank_id: "color2", answer: "black" }
], answers: @answers
)
expect(html).not_to match "{{"
end
end
context "multiple_dropdowns_question" do
before do
# double #user_content
def user_content(stuff)
stuff
end
end
it "selects the user's answer" do
html = multiple_dropdowns_question({
question: {
question_text: 'some <select class="question_input" name="question_4"><option value="val">val</option></select>'
},
answer_list: ["val"],
editable: true
})
expect(html).to eq 'some <select class="question_input" name="question_4" aria-label="Multiple dropdowns, read surrounding text"><option value="val" selected="selected">val</option></select>'
expect(html).to be_html_safe
end
it "does not blow up if the user's answer isn't there" do
html = multiple_dropdowns_question({
question: {
question_text: 'some <select class="question_input" name="question_4"><option value="other_val">val</option></select>'
},
answer_list: ["val"],
editable: true
})
expect(html).to eq 'some <select class="question_input" name="question_4" aria-label="Multiple dropdowns, read surrounding text"><option value="other_val">val</option></select>'
expect(html).to be_html_safe
end
it "disables select boxes that are not editable" do
html_string = multiple_dropdowns_question({
question: {
question_text: 'some <select class="question_input" name="question_4"><option value="val">val</option></select>'
},
answer_list: ["val"],
editable: false
})
html = Nokogiri::HTML.fragment(html_string)
span_html = html.css("span").first
expect(span_html).not_to be_nil
expect(html_string).to be_html_safe
end
end
describe "#quiz_edit_text" do
it "returns correct string for survey" do
quiz = double(survey?: true)
expect(quiz_edit_text(quiz)).to eq "Edit Survey"
end
it "returns correct string for quiz" do
quiz = double(survey?: false)
expect(quiz_edit_text(quiz)).to eq "Edit Quiz"
end
end
describe "#quiz_delete_text" do
it "returns correct string for survey" do
quiz = double(survey?: true)
expect(quiz_delete_text(quiz)).to eq "Delete Survey"
end
it "returns correct string for quiz" do
quiz = double(survey?: false)
expect(quiz_delete_text(quiz)).to eq "Delete Quiz"
end
end
describe "#score_affected_by_regrade" do
it "returns true if kept score differs from score before regrade" do
submission = double(score_before_regrade: 5, kept_score: 10, score: 5)
expect(score_affected_by_regrade?(submission)).to be_truthy
end
it "returns false if kept score equals score before regrade" do
submission = double(score_before_regrade: 5, kept_score: 5, score: 0)
expect(score_affected_by_regrade?(submission)).to be_falsey
end
end
describe "#answer_title" do
it "builds title if answer is selected" do
title = answer_title("foo", true, false, false)
expect(title).to eq "title=\"foo. You selected this answer.\""
end
it "builds title if answer is correct" do
title = answer_title("foo", false, true, true)
expect(title).to eq "title=\"foo. This was the correct answer.\""
end
it "returns nil if not selected or correct" do
title = answer_title("foo", false, false, false)
expect(title).to be_nil
end
end
describe "#render_show_correct_answers" do
context "show_correct_answers is false" do
it "shows No" do
quiz = double({ show_correct_answers: false })
expect(render_show_correct_answers(quiz)).to eq "No"
end
end
context "show_correct_answers is true, but nothing else is set" do
it "shows Immediately" do
quiz = double({
show_correct_answers: true,
show_correct_answers_at: nil,
hide_correct_answers_at: nil,
show_correct_answers_last_attempt: false
})
expect(render_show_correct_answers(quiz)).to eq "Immediately"
end
end
context "show_correct_answers_last_attempt is true" do
it "shows After Last Attempt" do
quiz = double({
show_correct_answers: true,
show_correct_answers_at: nil,
hide_correct_answers_at: nil,
show_correct_answers_last_attempt: true
})
expect(render_show_correct_answers(quiz)).to eq "After Last Attempt"
end
end
context "show_correct_answers_at is set" do
it "shows date of" do
time = 1.day.from_now
quiz = double({
show_correct_answers: true,
show_correct_answers_at: time,
hide_correct_answers_at: nil
})
expect(render_show_correct_answers(quiz)).to eq "After #{datetime_string(time)}"
end
end
context "hide_correct_answers_at is set" do
it "shows date of" do
time = 1.day.from_now
quiz = double({
show_correct_answers: true,
show_correct_answers_at: nil,
hide_correct_answers_at: time,
})
expect(render_show_correct_answers(quiz)).to eq "Until #{datetime_string(time)}"
end
end
context "show_correct_answers_at and hide_correct_answers_at are set" do
it "shows date of" do
time = 1.day.from_now
time2 = 1.week.from_now
quiz = double({
show_correct_answers: true,
show_correct_answers_at: time,
hide_correct_answers_at: time2,
})
expect(render_show_correct_answers(quiz)).to eq "From #{datetime_string(time)} to #{datetime_string(time2)}"
end
end
end
describe "#render_correct_answer_protection" do
it 'provides a useful message when "last attempt"' do
quiz = double({
show_correct_answers_last_attempt: true,
})
quiz_submission = double(last_attempt_completed?: false)
message = render_correct_answer_protection(quiz, quiz_submission)
expect(message).to match(/last attempt/)
end
it 'provides a useful message when "no"' do
quiz = double({
show_correct_answers_last_attempt: nil,
show_correct_answers: false,
show_correct_answers_at: nil,
hide_correct_answers_at: nil
})
quiz_submission = double(last_attempt_completed?: false)
message = render_correct_answer_protection(quiz, quiz_submission)
expect(message).to match(/are hidden/)
end
it 'provides nothing when "yes"' do
quiz = double({
show_correct_answers_last_attempt: nil,
show_correct_answers: true,
show_correct_answers_at: nil,
hide_correct_answers_at: nil
})
quiz_submission = double(last_attempt_completed?: false)
message = render_correct_answer_protection(quiz, quiz_submission)
expect(message).to eq nil
end
it 'provides a useful message, and an availability date, when "show at" is set' do
quiz = double({
show_correct_answers_last_attempt: nil,
show_correct_answers: true,
show_correct_answers_at: 1.day.from_now,
hide_correct_answers_at: nil
})
quiz_submission = double(last_attempt_completed?: false)
message = render_correct_answer_protection(quiz, quiz_submission)
expect(message).to match(/will be available/)
end
it 'provides a useful message, and a date, when "hide at" is set' do
quiz = double({
show_correct_answers_last_attempt: nil,
show_correct_answers: true,
show_correct_answers_at: nil,
hide_correct_answers_at: 1.day.from_now
})
quiz_submission = double(last_attempt_completed?: false)
message = render_correct_answer_protection(quiz, quiz_submission)
expect(message).to match(/are available until/)
end
end
context "#point_value_for_input" do
let(:user_answer) { @user_answer }
let(:question) { { points_possible: 5 } }
let(:quiz) { @quiz }
before do
@quiz = double(quiz_type: "graded_survey")
@user_answer = { correct: "undefined", points: 5 }
end
it "returns user_answer[:points] if correct is true/false" do
[true, false].each do |bool|
user_answer[:correct] = bool
expect(point_value_for_input(user_answer, question)).to eq user_answer[:points]
end
end
it "returns empty if quiz is practice quiz or assignment" do
["assignment", "practice_quiz"].each do |quiz_type|
expect(@quiz).to receive(:quiz_type).and_return quiz_type
expect(point_value_for_input(user_answer, question)).to eq ""
end
end
it "returns points possible for the question if (un)graded survey" do
["survey", "graded_survey"].each do |quiz_type|
expect(@quiz).to receive(:quiz_type).and_return quiz_type
expect(point_value_for_input(user_answer, question)).to eq(
question[:points_possible]
)
end
end
end
context "#comment_get" do
it "returns _html field if present" do
comment = comment_get({ foo_html: "<div>Foo</div>", foo: "Bar" }, "foo")
expect(comment).to eq "<div>Foo</div>"
end
it "returns raw field if _html field not present" do
comment = comment_get({ foo: "Bar" }, "foo")
expect(comment).to eq "Bar"
end
it "adds MathML if appropriate" do
comment = comment_get({
foo_html: '<img class="equation_image" data-equation-content="\coprod"></img>'
}, "foo")
expect(comment).to match(/MathML/)
expect(comment).to match(//)
end
it "does not add MathML if new math handling feature is active" do
def controller.use_new_math_equation_handling?
true
end
comment = comment_get({
foo_html: '<img class="equation_image" data-equation-content="\coprod"></img>'
}, "foo")
expect(comment).to eq('<img class="equation_image" data-equation-content="\\coprod">')
end
end
end