more code refactor to colocate feature code

This commit is contained in:
2025-07-23 11:25:12 -06:00
parent c37ad0708e
commit 815f929c2d
30 changed files with 535 additions and 646 deletions

View File

@@ -0,0 +1,236 @@
import { describe, it, expect } from "vitest";
import { LocalAssignment } from "../assignments/models/localAssignment";
import { AssignmentSubmissionType } from "../assignments/models/assignmentSubmissionType";
import { assignmentMarkdownSerializer } from "../assignments/models/utils/assignmentMarkdownSerializer";
import { assignmentMarkdownParser } from "../assignments/models/utils/assignmentMarkdownParser";
describe("AssignmentMarkdownTests", () => {
it("can parse assignment settings", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "here is the description",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
localAssignmentGroupName: "Final Project",
rubric: [
{ points: 4, label: "do task 1" },
{ points: 2, label: "do task 2" },
],
allowedFileUploadExtensions: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignment with empty rubric can be parsed", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "here is the description",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignment with empty submission types can be parsed", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "here is the description",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [
{ points: 4, label: "do task 1" },
{ points: 2, label: "do task 2" },
],
allowedFileUploadExtensions: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignment without lockAt date can be parsed", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "here is the description",
dueAt: "08/21/2023 23:59:00",
lockAt: undefined,
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [
{ points: 4, label: "do task 1" },
{ points: 2, label: "do task 2" },
],
allowedFileUploadExtensions: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignment without description can be parsed", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [
{ points: 4, label: "do task 1" },
{ points: 2, label: "do task 2" },
],
allowedFileUploadExtensions: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignments can have three dashes", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "test assignment\n---\nsomestuff",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignments can restrict upload types", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "here is the description",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
allowedFileUploadExtensions: ["pdf", "txt"],
localAssignmentGroupName: "Final Project",
rubric: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignment with githubClassroomAssignmentShareLink and githubClassroomAssignmentLink can be parsed", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "here is the description",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
githubClassroomAssignmentShareLink: "https://github.com/share-link",
githubClassroomAssignmentLink: "https://github.com/assignment-link",
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment.githubClassroomAssignmentShareLink).toEqual(
"https://github.com/share-link"
);
expect(parsedAssignment.githubClassroomAssignmentLink).toEqual(
"https://github.com/assignment-link"
);
expect(parsedAssignment).toEqual(assignment);
});
it("assignment without githubClassroomAssignmentShareLink and githubClassroomAssignmentLink can be parsed", () => {
const name = "test assignment";
const assignment: LocalAssignment = {
name,
description: "here is the description",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [AssignmentSubmissionType.ONLINE_UPLOAD],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const assignmentMarkdown =
assignmentMarkdownSerializer.toMarkdown(assignment);
const parsedAssignment = assignmentMarkdownParser.parseMarkdown(
assignmentMarkdown,
name
);
expect(parsedAssignment.githubClassroomAssignmentShareLink).toBeUndefined();
expect(parsedAssignment.githubClassroomAssignmentLink).toBeUndefined();
expect(parsedAssignment).toEqual(assignment);
});
});

View File

@@ -0,0 +1,23 @@
import { GlobalSettings } from "@/features/local/globalSettings/globalSettingsModels";
import { globalSettingsToYaml, parseGlobalSettingsYaml } from "@/features/local/globalSettings/globalSettingsUtils";
import { describe, it, expect } from "vitest";
describe("GlobalSettingsMarkdownTests", () => {
it("can parse global settings", () => {
const globalSettings: GlobalSettings = {
courses: [
{
path: "./distributed/2025-alex/modules",
name: "distributed",
},
],
};
const globalSettingsMarkdown = globalSettingsToYaml(globalSettings);
const parsedGlobalSettings = parseGlobalSettingsYaml(
globalSettingsMarkdown
);
expect(parsedGlobalSettings).toEqual(globalSettings);
});
});

View File

@@ -0,0 +1,19 @@
import { LocalCoursePage, localPageMarkdownUtils } from "@/features/local/pages/localCoursePageModels";
import { describe, it, expect } from "vitest";
describe("PageMarkdownTests", () => {
it("can parse page", () => {
const name = "test title"
const page: LocalCoursePage = {
name,
text: "test text content",
dueAt: "07/09/2024 23:59:00",
};
const pageMarkdownString = localPageMarkdownUtils.toMarkdown(page);
const parsedPage = localPageMarkdownUtils.parseMarkdown(pageMarkdownString, name);
expect(parsedPage).toEqual(page);
});
});

View File

@@ -0,0 +1,25 @@
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { describe, it, expect } from "vitest";
describe("Matching Answer Error Messages", () => {
it("can parse matching question", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
question without answer
`;
expect(() =>
quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name)
).toThrowError(/question type/);
});
});

View File

@@ -0,0 +1,165 @@
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
import { describe, it, expect } from "vitest";
describe("MatchingTests", () => {
it("can parse matching question", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.questionType).toBe(QuestionType.MATCHING);
expect(firstQuestion.text).not.toContain("statement");
expect(firstQuestion.answers[0].matchedText).toBe(
"a single command to be executed"
);
});
it("can create markdown for matching question", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const questionMarkdown = quizQuestionMarkdownUtils.toMarkdown(
quiz.questions[0]
);
const expectedMarkdown = `Points: 1
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)`;
expect(questionMarkdown).toContain(expectedMarkdown);
});
it("whitespace is optional", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^statement - a single command to be executed
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
expect(quiz.questions[0].answers[0].text).toBe("statement");
});
it("can have distractors", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ - this is the distractor
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
expect(quiz.questions[0].matchDistractors).toEqual([
"this is the distractor",
]);
});
it("can have distractors and be persisted", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ - this is the distractor
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
expect(quizMarkdown).toContain(
"^ statement - a single command to be executed\n^ - this is the distractor"
);
});
it("can escape - characters", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ git add \-\-all - start tracking all files in the current directory and subdirectories
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.answers[0].text).toBe("git add --all");
expect(firstQuestion.answers[0].matchedText).toBe(
"start tracking all files in the current directory and subdirectories"
);
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
expect(quizMarkdown).toContain(
"^ git add --all - start tracking all files in the current directory and subdirectories"
);
});
});

View File

@@ -0,0 +1,181 @@
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
import { describe, it, expect } from "vitest";
describe("MultipleAnswersTests", () => {
it("quiz markdown includes multiple answer question", () => {
const quiz: LocalQuiz = {
name: "Test Quiz",
description: "desc",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: false,
showCorrectAnswers: false,
localAssignmentGroupName: "someId",
allowedAttempts: -1,
questions: [
{
text: "oneline question",
points: 1,
questionType: QuestionType.MULTIPLE_ANSWERS,
answers: [
{ correct: true, text: "true" },
{ correct: true, text: "false" },
{ correct: false, text: "neither" },
],
matchDistractors: [],
},
],
};
const markdown = quizMarkdownUtils.toMarkdown(quiz);
const expectedQuestionString = `Points: 1
oneline question
[*] true
[*] false
[ ] neither`;
expect(markdown).toContain(expectedQuestionString);
});
it("can parse question with multiple answers", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
[*] click
[*] focus
[*] mousedown
[ ] submit
[ ] change
[ ] mouseout
[ ] keydown
---
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.points).toBe(1);
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_ANSWERS);
expect(firstQuestion.text).toContain(
"Which events are triggered when the user clicks on an input field?"
);
expect(firstQuestion.answers[0].text).toBe("click");
expect(firstQuestion.answers[0].correct).toBe(true);
expect(firstQuestion.answers[3].correct).toBe(false);
expect(firstQuestion.answers[3].text).toBe("submit");
});
it("can parse question with multiple answers without a space in false answers", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
[*] click
[] submit
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.answers.length).toBe(2);
expect(firstQuestion.answers[0].correct).toBe(true);
expect(firstQuestion.answers[1].correct).toBe(false);
});
it("can parse question with multiple answers without a space in false answers other example", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: 1
Which tool(s) will let you: create a database migration or reverse-engineer an existing database
[] swagger
[] a .http file
[*] dotnet ef command line interface
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.answers.length).toBe(3);
expect(firstQuestion.answers[0].correct).toBe(false);
expect(firstQuestion.answers[1].correct).toBe(false);
expect(firstQuestion.answers[2].correct).toBe(true);
});
it("can use braces in answer for multiple answer", () => {
const rawMarkdownQuestion = `
Which events are triggered when the user clicks on an input field?
[*] \`int[] theThing()\`
[ ] keydown
`;
const question = quizQuestionMarkdownUtils.parseMarkdown(
rawMarkdownQuestion,
0
);
expect(question.answers[0].text).toBe("`int[] theThing()`");
expect(question.answers.length).toBe(2);
});
it("can use braces in answer for multiple answer with multiline", () => {
const rawMarkdownQuestion = `
Which events are triggered when the user clicks on an input field?
[*]
\`\`\`
int[] myNumbers = new int[] { };
DoSomething(ref myNumbers);
static void DoSomething(ref int[] numbers)
{
// do something
}
\`\`\`
`;
const question = quizQuestionMarkdownUtils.parseMarkdown(
rawMarkdownQuestion,
0
);
expect(question.answers[0].text).toBe(`\`\`\`
int[] myNumbers = new int[] { };
DoSomething(ref myNumbers);
static void DoSomething(ref int[] numbers)
{
// do something
}
\`\`\``);
expect(question.answers.length).toBe(1);
});
});

View File

@@ -0,0 +1,74 @@
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
import { describe, it, expect } from "vitest";
describe("MultipleChoiceTests", () => {
it("quiz markdown includes multiple choice question", () => {
const quiz: LocalQuiz = {
name: "Test Quiz",
description: "desc",
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: false,
showCorrectAnswers: false,
localAssignmentGroupName: "someId",
allowedAttempts: -1,
questions: [
{
points: 2,
text: `
\`some type\` of question
with many
\`\`\`
lines
\`\`\`
`,
questionType: QuestionType.MULTIPLE_CHOICE,
answers: [
{ correct: true, text: "true" },
{ correct: false, text: "false\n\nendline" },
],
matchDistractors: [],
},
],
};
const markdown = quizMarkdownUtils.toMarkdown(quiz);
const expectedQuestionString = `
Points: 2
\`some type\` of question
with many
\`\`\`
lines
\`\`\`
*a) true
b) false
endline`;
expect(markdown).toContain(expectedQuestionString);
});
it("letter optional for multiple choice", () => {
const questionMarkdown = `
Points: 2
\`some type\` of question
*) true
) false
`;
const question = quizQuestionMarkdownUtils.parseMarkdown(
questionMarkdown,
0
);
expect(question.answers.length).toBe(2);
});
});

View File

@@ -0,0 +1,205 @@
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { describe, it, expect } from "vitest";
// Test suite for deterministic checks on LocalQuiz
describe("QuizDeterministicChecks", () => {
it("SerializationIsDeterministic_EmptyQuiz", () => {
const name = "Test Quiz";
const quiz: LocalQuiz = {
name,
description: "quiz description",
lockAt: "08/21/2023 23:59:00",
dueAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: true,
localAssignmentGroupName: "Assignments",
questions: [],
allowedAttempts: -1,
showCorrectAnswers: true,
};
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
expect(parsedQuiz).toEqual(quiz);
});
it("SerializationIsDeterministic_ShowCorrectAnswers", () => {
const name = "Test Quiz";
const quiz: LocalQuiz = {
name,
description: "quiz description",
lockAt: "08/21/2023 23:59:00",
dueAt: "08/21/2023 23:59:00",
showCorrectAnswers: false,
shuffleAnswers: true,
oneQuestionAtATime: true,
localAssignmentGroupName: "Assignments",
questions: [],
allowedAttempts: -1,
};
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
expect(parsedQuiz).toEqual(quiz);
});
it("SerializationIsDeterministic_ShortAnswer", () => {
const name = "Test Quiz";
const quiz: LocalQuiz = {
name,
description: "quiz description",
lockAt: "08/21/2023 23:59:00",
dueAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: true,
localAssignmentGroupName: "Assignments",
questions: [
{
text: "test short answer",
questionType: QuestionType.SHORT_ANSWER,
points: 1,
answers: [],
matchDistractors: [],
},
],
allowedAttempts: -1,
showCorrectAnswers: true,
};
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
expect(parsedQuiz).toEqual(quiz);
});
it("SerializationIsDeterministic_Essay", () => {
const name = "Test Quiz";
const quiz: LocalQuiz = {
name,
description: "quiz description",
lockAt: "08/21/2023 23:59:00",
dueAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: true,
localAssignmentGroupName: "Assignments",
questions: [
{
text: "test essay",
questionType: QuestionType.ESSAY,
points: 1,
matchDistractors: [],
answers: [],
},
],
allowedAttempts: -1,
showCorrectAnswers: true,
};
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
expect(parsedQuiz).toEqual(quiz);
});
it("SerializationIsDeterministic_MultipleAnswer", () => {
const name = "Test Quiz";
const quiz: LocalQuiz = {
name,
description: "quiz description",
lockAt: "08/21/2023 23:59:00",
dueAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: true,
localAssignmentGroupName: "Assignments",
questions: [
{
text: "test multiple answer",
questionType: QuestionType.MULTIPLE_ANSWERS,
points: 1,
matchDistractors: [],
answers: [
{ text: "yes", correct: true },
{ text: "no", correct: true },
],
},
],
allowedAttempts: -1,
showCorrectAnswers: true,
};
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
expect(parsedQuiz).toEqual(quiz);
});
it("SerializationIsDeterministic_MultipleChoice", () => {
const name = "Test Quiz";
const quiz: LocalQuiz = {
name,
description: "quiz description",
lockAt: "08/21/2023 23:59:00",
dueAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: true,
password: undefined,
localAssignmentGroupName: "Assignments",
questions: [
{
text: "test multiple choice",
questionType: QuestionType.MULTIPLE_CHOICE,
points: 1,
matchDistractors: [],
answers: [
{ text: "yes", correct: true },
{ text: "no", correct: false },
],
},
],
allowedAttempts: -1,
showCorrectAnswers: true,
};
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
expect(parsedQuiz).toEqual(quiz);
});
it("SerializationIsDeterministic_Matching", () => {
const name = "Test Quiz";
const quiz: LocalQuiz = {
name,
description: "quiz description",
lockAt: "08/21/2023 23:59:00",
dueAt: "08/21/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: true,
password: undefined,
localAssignmentGroupName: "Assignments",
questions: [
{
text: "test matching",
questionType: QuestionType.MATCHING,
points: 1,
matchDistractors: [],
answers: [
{ text: "yes", correct: true, matchedText: "testing yes" },
{ text: "no", correct: true, matchedText: "testing no" },
],
},
],
allowedAttempts: -1,
showCorrectAnswers: true,
};
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
const parsedQuiz = quizMarkdownUtils.parseMarkdown(quizMarkdown, name);
expect(parsedQuiz).toEqual(quiz);
});
});

View File

@@ -0,0 +1,284 @@
import { describe, it, expect } from "vitest";
import { markdownToHtmlNoImages } from "@/services/htmlMarkdownUtils";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
// Test suite for QuizMarkdown
describe("QuizMarkdownTests", () => {
it("can serialize quiz to markdown", () => {
const quiz: LocalQuiz = {
name: "Test Quiz",
description: `
# quiz description
this is my description in markdown
\`here is code\`
`,
lockAt: new Date(8640000000000000).toISOString(), // DateTime.MaxValue equivalent in TypeScript
dueAt: new Date(8640000000000000).toISOString(),
shuffleAnswers: true,
oneQuestionAtATime: false,
localAssignmentGroupName: "someId",
allowedAttempts: -1,
showCorrectAnswers: false,
questions: [],
};
const markdown = quizMarkdownUtils.toMarkdown(quiz);
expect(markdown).not.toContain("Name: Test Quiz");
expect(markdown).toContain(quiz.description);
expect(markdown).toContain("ShuffleAnswers: true");
expect(markdown).toContain("OneQuestionAtATime: false");
expect(markdown).toContain("AssignmentGroup: someId");
expect(markdown).toContain("AllowedAttempts: -1");
});
it("can parse markdown quiz with no questions", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const expectedDescription = `
this is the
multi line
description`;
expect(quiz.name).toBe("Test Quiz");
expect(quiz.shuffleAnswers).toBe(true);
expect(quiz.oneQuestionAtATime).toBe(false);
expect(quiz.allowedAttempts).toBe(-1);
expect(quiz.description.trim()).toBe(expectedDescription.trim());
});
it("can parse markdown quiz with password", () => {
const password = "this-is-the-password";
const name = "Test Quiz";
const rawMarkdownQuiz = `
Password: ${password}
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
expect(quiz.password).toBe(password);
});
it("can parse markdown quiz and configure to show correct answers", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
ShowCorrectAnswers: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
expect(quiz.showCorrectAnswers).toBe(false);
});
it("can parse quiz with questions", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: 2
\`some type\` of question
with many
\`\`\`
lines
\`\`\`
*a) true
b) false
endline`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_CHOICE);
expect(firstQuestion.points).toBe(2);
expect(firstQuestion.text).toContain("```");
expect(firstQuestion.text).toContain("`some type` of question");
expect(firstQuestion.answers[0].text).toBe("true");
expect(firstQuestion.answers[0].correct).toBe(true);
expect(firstQuestion.answers[1].correct).toBe(false);
expect(firstQuestion.answers[1].text).toContain("endline");
});
it("can parse multiple questions", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
[*] click
---
Points: 2
\`some type\` of question
*a) true
b) false
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.points).toBe(1);
expect(firstQuestion.questionType).toBe(QuestionType.MULTIPLE_ANSWERS);
const secondQuestion = quiz.questions[1];
expect(secondQuestion.points).toBe(2);
expect(secondQuestion.questionType).toBe(QuestionType.MULTIPLE_CHOICE);
});
it("short answer to markdown is correct", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
short answer
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
const questionMarkdown =
quizQuestionMarkdownUtils.toMarkdown(firstQuestion);
const expectedMarkdown = `Points: 1
Which events are triggered when the user clicks on an input field?
short_answer`;
expect(questionMarkdown).toContain(expectedMarkdown);
});
it("negative points is allowed", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: -4
Which events are triggered when the user clicks on an input field?
short answer
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.points).toBe(-4);
});
it("floating point points is allowed", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: 4.56
Which events are triggered when the user clicks on an input field?
short answer
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.points).toBe(4.56);
});
it("can parse quiz with latex in a question", () => {
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: 2
This is latex: $x_2$
*a) true
b) false
endline`;
const quizHtml = markdownToHtmlNoImages(rawMarkdownQuiz);
expect(quizHtml).not.toContain("$");
expect(quizHtml).toContain("<mi>x</mi>");
expect(quizHtml).not.toContain("x_2");
});
});

View File

@@ -0,0 +1,278 @@
import { QuestionType, zodQuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils";
import { quizQuestionMarkdownUtils } from "@/features/local/quizzes/models/utils/quizQuestionMarkdownUtils";
import {
getAnswers,
getQuestionType,
} from "@/services/canvas/canvasQuizService";
import { describe, it, expect } from "vitest";
describe("TextAnswerTests", () => {
it("can parse essay", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
essay
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.points).toBe(1);
expect(firstQuestion.questionType).toBe(QuestionType.ESSAY);
expect(firstQuestion.text).not.toContain("essay");
});
it("can parse short answer", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
short answer
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.points).toBe(1);
expect(firstQuestion.questionType).toBe(QuestionType.SHORT_ANSWER);
expect(firstQuestion.text).not.toContain("short answer");
});
it("short answer to markdown is correct", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
short answer
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
const questionMarkdown =
quizQuestionMarkdownUtils.toMarkdown(firstQuestion);
const expectedMarkdown = `Points: 1
Which events are triggered when the user clicks on an input field?
short_answer`;
expect(questionMarkdown).toContain(expectedMarkdown);
});
it("short_answer= to markdown is correct", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
*a) yes
*b) Yes
short_answer=
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
const questionMarkdown =
quizQuestionMarkdownUtils.toMarkdown(firstQuestion);
const expectedMarkdown = `Points: 1
Which events are triggered when the user clicks on an input field?
*a) yes
*b) Yes
short_answer=`;
expect(questionMarkdown).toContain(expectedMarkdown);
});
it("essay question to markdown is correct", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
essay
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
const questionMarkdown =
quizQuestionMarkdownUtils.toMarkdown(firstQuestion);
const expectedMarkdown = `Points: 1
Which events are triggered when the user clicks on an input field?
essay`;
expect(questionMarkdown).toContain(expectedMarkdown);
});
it("Can parse short answer with auto graded answers", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
*a) test
*b) other
short_answer=
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.questionType).toBe(
QuestionType.SHORT_ANSWER_WITH_ANSWERS
);
expect(firstQuestion.answers.length).toBe(2);
expect(firstQuestion.answers[0].text).toBe("test");
expect(firstQuestion.answers[1].text).toBe("other");
});
it("Can parse short answer with auto graded answers", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
*a) test
*b) other
short_answer=
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(firstQuestion.questionType).toBe(
QuestionType.SHORT_ANSWER_WITH_ANSWERS
);
expect(firstQuestion.answers.length).toBe(2);
expect(firstQuestion.answers[0].text).toBe("test");
expect(firstQuestion.answers[1].text).toBe("other");
});
it("Has short_answer= type at the same position in types and zod types", () => {
expect(Object.values(zodQuestionType.Enum)).toEqual(
Object.values(QuestionType)
);
});
it("Associates short_answer= questions with short_answer_question canvas question type", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
*a) test
*b) other
short_answer=
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
expect(getQuestionType(firstQuestion)).toBe("short_answer_question");
});
it("Includes answer_text in answers sent to canvas", () => {
const name = "Test Quiz";
const rawMarkdownQuiz = `
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 08/21/2023 23:59:00
LockAt: 08/21/2023 23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
*a) test
*b) other
short_answer=
`;
const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name);
const firstQuestion = quiz.questions[0];
const answers = getAnswers(firstQuestion, {
name: "",
assignmentGroups: [],
daysOfWeek: [],
canvasId: 0,
startDate: "",
endDate: "",
defaultDueTime: {
hour: 0,
minute: 0,
},
defaultAssignmentSubmissionTypes: [],
defaultFileUploadTypes: [],
holidays: [],
assets: [],
});
expect(answers).toHaveLength(2);
const firstAnswer = answers[0];
expect(firstAnswer).toHaveProperty("answer_text");
});
});

View File

@@ -0,0 +1,100 @@
import { describe, it, expect } from "vitest";
import {
RubricItem,
rubricItemIsExtraCredit,
} from "../assignments/models/rubricItem";
import { assignmentMarkdownParser } from "../assignments/models/utils/assignmentMarkdownParser";
describe("RubricMarkdownTests", () => {
it("can parse one item", () => {
const rawRubric = `
- 2pts: this is the task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubric.length).toBe(1);
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
expect(rubric[0].label).toBe("this is the task");
expect(rubric[0].points).toBe(2);
});
it("can parse multiple items", () => {
const rawRubric = `
- 2pts: this is the task
- 3pts: this is the other task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubric.length).toBe(2);
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
expect(rubric[1].label).toBe("this is the other task");
expect(rubric[1].points).toBe(3);
});
it("can parse single point", () => {
const rawRubric = `
- 1pt: this is the task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubricItemIsExtraCredit(rubric[0])).toBe(false);
expect(rubric[0].label).toBe("this is the task");
expect(rubric[0].points).toBe(1);
});
it("can parse single extra credit (lower case)", () => {
const rawRubric = `
- 1pt: (extra credit) this is the task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubricItemIsExtraCredit(rubric[0])).toBe(true);
expect(rubric[0].label).toBe("(extra credit) this is the task");
});
it("can parse single extra credit (upper case)", () => {
const rawRubric = `
- 1pt: (Extra Credit) this is the task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubricItemIsExtraCredit(rubric[0])).toBe(true);
expect(rubric[0].label).toBe("(Extra Credit) this is the task");
});
it("can parse floating point numbers", () => {
const rawRubric = `
- 1.5pt: this is the task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubric[0].points).toBe(1.5);
});
it("can parse negative numbers", () => {
const rawRubric = `
- -2pt: this is the task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubric[0].points).toBe(-2);
});
it("can parse negative floating point numbers", () => {
const rawRubric = `
- -2895.00053pt: this is the task
`;
const rubric: RubricItem[] =
assignmentMarkdownParser.parseRubricMarkdown(rawRubric);
expect(rubric[0].points).toBe(-2895.00053);
});
});

View File

@@ -0,0 +1,31 @@
import { describe, it, expect } from "vitest";
import { parseHolidays } from "../utils/settingsUtils";
describe("can parse holiday string", () => {
it("can parse empty list", () => {
const testString = `
springBreak:
`;
const output = parseHolidays(testString);
expect(output).toEqual([{ name: "springBreak", days: [] }]);
});
it("can parse list with date", () => {
const testString = `
springBreak:
- 10/12/2024
`;
const output = parseHolidays(testString);
expect(output).toEqual([{ name: "springBreak", days: ["10/12/2024"] }]);
});
it("can parse list with two dates", () => {
const testString = `
springBreak:
- 10/12/2024
- 10/13/2024
`;
const output = parseHolidays(testString);
expect(output).toEqual([
{ name: "springBreak", days: ["10/12/2024", "10/13/2024"] },
]);
});
});

View File

@@ -0,0 +1,222 @@
import { describe, it, expect } from "vitest";
import { LocalAssignment } from "../assignments/models/localAssignment";
import {
prepAssignmentForNewSemester,
prepLectureForNewSemester,
prepPageForNewSemester,
prepQuizForNewSemester,
} from "../utils/semesterTransferUtils";
import { Lecture } from "../lectures/lectureModel";
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
describe("can take an assignment and template it for a new semester", () => {
it("can sanitize assignment github classroom repo url", () => {
const assignment: LocalAssignment = {
name: "test assignment",
description: `
## test description
[GitHub Classroom Assignment](https://classroom.github.com/a/y_eOxTfL)
other stuff below`,
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedAssignment = prepAssignmentForNewSemester(
assignment,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedAssignment.description).toEqual(`
## test description
[GitHub Classroom Assignment](insert_github_classroom_url)
other stuff below`);
});
it("can sanitize assignment github classroom repo url 2", () => {
const assignment: LocalAssignment = {
name: "test assignment",
description: `
<https://classroom.github.com/a/y_eOxTfL>
other stuff below`,
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedAssignment = prepAssignmentForNewSemester(
assignment,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedAssignment.description).toEqual(`
<insert_github_classroom_url>
other stuff below`);
});
it("can sanitize assignment github classroom repo url 3", () => {
const assignment: LocalAssignment = {
name: "test assignment",
description: `https://classroom.github.com/a/y_eOxTfL other things`,
dueAt: "08/21/2023 23:59:00",
lockAt: "08/21/2023 23:59:00",
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedAssignment = prepAssignmentForNewSemester(
assignment,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedAssignment.description).toEqual(
`insert_github_classroom_url other things`
);
});
});
describe("can offset date based on new semester start", () => {
it("assignment with new semester start", () => {
const assignment: LocalAssignment = {
name: "test assignment",
description: `https://classroom.github.com/a/y_eOxTfL other things`,
dueAt: "08/29/2023 23:59:00",
lockAt: "08/29/2023 23:59:00",
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedAssignment = prepAssignmentForNewSemester(
assignment,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedAssignment.dueAt).toEqual("01/11/2024 23:59:00");
expect(sanitizedAssignment.lockAt).toEqual("01/11/2024 23:59:00");
});
it("assignment with new semester start, no lock date", () => {
const assignment: LocalAssignment = {
name: "test assignment",
description: `https://classroom.github.com/a/y_eOxTfL other things`,
dueAt: "08/29/2023 23:59:00",
lockAt: undefined,
submissionTypes: [],
localAssignmentGroupName: "Final Project",
rubric: [],
allowedFileUploadExtensions: [],
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedAssignment = prepAssignmentForNewSemester(
assignment,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedAssignment.dueAt).toEqual("01/11/2024 23:59:00");
expect(sanitizedAssignment.lockAt).toEqual(undefined);
});
});
describe("can prep quizzes", () => {
it("quiz gets new lock and due dates", () => {
const quiz: LocalQuiz = {
name: "Test Quiz",
description: `
# quiz description
`,
dueAt: "08/29/2023 23:59:00",
lockAt: "08/30/2023 23:59:00",
shuffleAnswers: true,
oneQuestionAtATime: false,
localAssignmentGroupName: "someId",
allowedAttempts: -1,
showCorrectAnswers: false,
questions: [],
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedQuiz = prepQuizForNewSemester(
quiz,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedQuiz.dueAt).toEqual("01/11/2024 23:59:00");
expect(sanitizedQuiz.lockAt).toEqual("01/12/2024 23:59:00");
});
});
describe("can prep pages", () => {
it("page gets new due date and github url changes", () => {
const page: LocalCoursePage = {
name: "test title",
text: "test text content",
dueAt: "08/30/2023 23:59:00",
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedPage = prepPageForNewSemester(
page,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedPage.dueAt).toEqual("01/12/2024 23:59:00");
});
});
describe("can prep lecture", () => {
it("lecture gets new date, github url changes", () => {
const lecture: Lecture = {
name: "test title",
date: "08/30/2023",
content: "test text content",
};
const oldSemesterStartDate = "08/26/2023 23:59:00";
const newSemesterStartDate = "01/08/2024 23:59:00";
const sanitizedLecture = prepLectureForNewSemester(
lecture,
oldSemesterStartDate,
newSemesterStartDate
);
expect(sanitizedLecture.date).toEqual("01/12/2024");
});
});

View File

@@ -0,0 +1,71 @@
import { describe, it, expect } from "vitest";
import { dateToMarkdownString, getDateFromString } from "../utils/timeUtils";
describe("Can properly handle expected date formats", () => {
it("can use AM/PM dates", () => {
const dateString = "8/27/2024 1:00:00AM";
const dateObject = getDateFromString(dateString);
expect(dateObject).not.toBeUndefined();
});
it("can use 24 hour dates", () => {
const dateString = "8/27/2024 23:95:00";
const dateObject = getDateFromString(dateString);
expect(dateObject).not.toBeUndefined();
});
it("can use ISO format", () => {
const dateString = "2024-08-26T00:00:00.0000000";
const dateObject = getDateFromString(dateString);
expect(dateObject).not.toBeUndefined();
});
it("can use other ISO format", () => {
const dateString = "2024-08-26T06:00:00Z";
const dateObject = getDateFromString(dateString);
expect(dateObject).not.toBeUndefined();
});
it("can get correct time from format", () => {
const dateString = "08/28/2024 23:59:00";
const dateObject = getDateFromString(dateString);
expect(dateObject?.getDate()).toBe(28);
expect(dateObject?.getMonth()).toBe(8 - 1); // 0 based
expect(dateObject?.getFullYear()).toBe(2024);
expect(dateObject?.getMinutes()).toBe(59);
expect(dateObject?.getHours()).toBe(23);
expect(dateObject?.getSeconds()).toBe(0);
});
it("can get correct time from format", () => {
const dateString = "8/27/2024 1:00:00AM";
const dateObject = getDateFromString(dateString);
expect(dateObject?.getDate()).toBe(27);
expect(dateObject?.getMonth()).toBe(8 - 1); // 0 based
expect(dateObject?.getFullYear()).toBe(2024);
expect(dateObject?.getMinutes()).toBe(0);
expect(dateObject?.getHours()).toBe(1);
expect(dateObject?.getSeconds()).toBe(0);
});
it("can get correct time from format", () => {
const dateString = "08/27/2024 23:59:00";
const dateObject = getDateFromString(dateString);
expect(dateObject).not.toBeUndefined();
const updatedString = dateToMarkdownString(dateObject!);
expect(updatedString).toBe(dateString);
});
it("can handle canvas time format", () => {
const dateString = "8/29/2024, 5:00:00 PM";
const dateObject = getDateFromString(dateString);
expect(dateObject).not.toBeUndefined();
const updatedString = dateToMarkdownString(dateObject!);
expect(updatedString).toBe("08/29/2024 17:00:00");
});
it("can handle date without time", () => {
const dateString = "8/29/2024";
const dateObject = getDateFromString(dateString);
expect(dateObject).not.toBeUndefined();
const updatedString = dateToMarkdownString(dateObject!);
expect(updatedString).toBe("08/29/2024 00:00:00");
});
});