Files
sm-quizzes-plugin/models/quiz_test.go
Jakub Zych 297780cb8e feat: add open questions with a pending grading state
- migration: open question type, rubric, max_length, quiz grader and
  the grading and review columns of attempt answers
- open answers are checked, stored as pending and never scored on
  answer; a changed text resets the grading state
- student JSON gains text, grading_status, grader_feedback,
  review_comment, max_length, needs_review and open_answers; the
  rubric is never serialized
2026-11-09 17:48:11 +01:00

74 lines
2.7 KiB
Go

package models
import (
"slices"
"strings"
"testing"
"git.golem15.com/golem15/sm-quizzes-plugin/graders"
)
func TestQuizDefaults(t *testing.T) {
zero, negative := 0, -1
for name, hook := range map[string]func(*Quiz) error{
"BeforeSave": func(q *Quiz) error { return q.BeforeSave(nil) },
"BeforeValidate": func(q *Quiz) error { return q.BeforeValidate(nil) },
} {
for _, limit := range []*int{&zero, &negative} {
q := &Quiz{Title: "Q", MaxAttempts: limit}
if err := hook(q); err != nil {
t.Fatalf("%s: %v", name, err)
}
if q.Mode != ModeLearning || q.Navigation != NavigationFree || q.MaxAttempts != nil {
t.Errorf("%s with max_attempts %d = %q %q %v, want learning free nil", name, *limit, q.Mode, q.Navigation, q.MaxAttempts)
}
if q.Grader != graders.Manual {
t.Errorf("%s left grader %q, want %q", name, q.Grader, graders.Manual)
}
}
two := 2
q := &Quiz{Title: "Q", Mode: ModeTest, Navigation: NavigationOneWay, MaxAttempts: &two, Grader: "ai"}
if err := hook(q); err != nil {
t.Fatalf("%s: %v", name, err)
}
if q.Mode != ModeTest || q.Navigation != NavigationOneWay || q.MaxAttempts == nil || *q.MaxAttempts != 2 {
t.Errorf("%s changed a test quiz: %q %q %v", name, q.Mode, q.Navigation, q.MaxAttempts)
}
if q.Grader != "ai" {
t.Errorf("%s changed an explicit grader to %q", name, q.Grader)
}
}
}
func TestOpenQuestionModel(t *testing.T) {
if !slices.Contains(QuestionTypes, TypeOpen) || len(QuestionTypes) != 4 {
t.Errorf("QuestionTypes = %v, want the open type appended", QuestionTypes)
}
if QuestionTypes[0] != TypeSingle || QuestionTypes[1] != TypeMultiple || QuestionTypes[2] != TypeSelfCheck {
t.Errorf("QuestionTypes reordered the existing types: %v", QuestionTypes)
}
rules := Question{}.Rules()
if !strings.Contains(rules["type"], "open") || !strings.Contains(rules["type"], "self_check") {
t.Errorf("type rule = %q, want every type including open", rules["type"])
}
if rules["max_length"] != "nullable|integer|between:1,20000" {
t.Errorf("max_length rule = %q", rules["max_length"])
}
fill := Question{}.Fillable()
if !slices.Contains(fill, "rubric") || !slices.Contains(fill, "max_length") {
t.Errorf("Fillable = %v, want rubric and max_length", fill)
}
if DefaultMaxLength != 2000 || MaxMaxLength != 20000 {
t.Errorf("max length limits = %d / %d", DefaultMaxLength, MaxMaxLength)
}
if len(GradingStatuses) != 4 || GradingStatuses[0] != GradingPending {
t.Errorf("GradingStatuses = %v", GradingStatuses)
}
if !slices.Contains(Quiz{}.Fillable(), "grader") {
t.Error("Quiz.Fillable lacks grader")
}
if len(AttemptAnswer{}.Fillable()) != 0 {
t.Error("AttemptAnswer.Fillable must stay empty")
}
}