diff --git a/src/features/canvas/services/quizOrderVerification.integration.test.ts b/src/features/canvas/services/quizOrderVerification.integration.test.ts deleted file mode 100644 index eb7ac26..0000000 --- a/src/features/canvas/services/quizOrderVerification.integration.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz"; -import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion"; -import { DayOfWeek } from "@/features/local/course/localCourseSettings"; -import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType"; - -// Mock the dependencies -vi.mock("@/services/axiosUtils", () => ({ - axiosClient: { - get: vi.fn(), - post: vi.fn(), - delete: vi.fn(), - }, -})); - -vi.mock("./canvasServiceUtils", () => ({ - canvasApi: "https://test.instructure.com/api/v1", - paginatedRequest: vi.fn(), -})); - -vi.mock("./canvasAssignmentService", () => ({ - canvasAssignmentService: { - getAll: vi.fn(() => Promise.resolve([])), - delete: vi.fn(() => Promise.resolve()), - }, -})); - -vi.mock("@/services/htmlMarkdownUtils", () => ({ - markdownToHTMLSafe: vi.fn(({ markdownString }) => `
${markdownString}
`), -})); - -vi.mock("@/features/local/utils/timeUtils", () => ({ - getDateFromStringOrThrow: vi.fn((dateString) => new Date(dateString)), -})); - -vi.mock("@/services/utils/questionHtmlUtils", () => ({ - escapeMatchingText: vi.fn((text) => text), -})); - -describe("Quiz Order Verification Integration", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("demonstrates the question order verification workflow", async () => { - // This test demonstrates that the verification step is properly integrated - // into the quiz creation workflow - - const testQuiz: LocalQuiz = { - name: "Test Quiz - Order Verification", - description: "Testing question order verification", - dueAt: "2023-12-01T23:59:00Z", - shuffleAnswers: false, - showCorrectAnswers: true, - oneQuestionAtATime: false, - allowedAttempts: 1, - questions: [ - { - text: "First Question", - questionType: QuestionType.SHORT_ANSWER, - points: 5, - answers: [], - matchDistractors: [], - }, - { - text: "Second Question", - questionType: QuestionType.ESSAY, - points: 10, - answers: [], - matchDistractors: [], - }, - ], - }; - - // Import the service after mocks are set up - const { canvasQuizService } = await import("./canvasQuizService"); - const { axiosClient } = await import("@/services/axiosUtils"); - const { paginatedRequest } = await import("./canvasServiceUtils"); - - // Mock successful quiz creation - vi.mocked(axiosClient.post).mockResolvedValueOnce({ - data: { id: 123, title: "Test Quiz - Order Verification" }, - }); - - // Mock question creation responses - vi.mocked(axiosClient.post) - .mockResolvedValueOnce({ data: { id: 1, position: 1 } }) - .mockResolvedValueOnce({ data: { id: 2, position: 2 } }); - - // Mock reordering call - vi.mocked(axiosClient.post).mockResolvedValueOnce({ data: {} }); - - // Mock assignment cleanup (empty assignments) - vi.mocked(paginatedRequest).mockResolvedValueOnce([]); - - // Mock the verification call - questions in correct order - vi.mocked(paginatedRequest).mockResolvedValueOnce([ - { - id: 1, - quiz_id: 123, - position: 1, - question_name: "Question 1", - question_type: "short_answer_question", - question_text: "First Question
", - correct_comments: "", - incorrect_comments: "", - neutral_comments: "", - }, - { - id: 2, - quiz_id: 123, - position: 2, - question_name: "Question 2", - question_type: "essay_question", - question_text: "Second Question
", - correct_comments: "", - incorrect_comments: "", - neutral_comments: "", - }, - ]); - - // Create the quiz and trigger verification - const result = await canvasQuizService.create(12345, testQuiz, { - name: "Test Course", - canvasId: 12345, - assignmentGroups: [], - daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday], - startDate: "2023-08-15", - endDate: "2023-12-15", - defaultDueTime: { hour: 23, minute: 59 }, - defaultAssignmentSubmissionTypes: [AssignmentSubmissionType.ONLINE_TEXT_ENTRY], - defaultFileUploadTypes: [], - holidays: [], - assets: [] - }); - - // Verify the quiz was created - expect(result).toBe(123); - - // Verify that the question verification API call was made - expect(vi.mocked(paginatedRequest)).toHaveBeenCalledWith({ - url: "https://test.instructure.com/api/v1/courses/12345/quizzes/123/questions", - }); - - // The verification would have run and logged success/failure - // In a real scenario, this would catch order mismatches - }); - - it("demonstrates successful verification workflow", async () => { - const { canvasQuizService } = await import("./canvasQuizService"); - const { paginatedRequest } = await import("./canvasServiceUtils"); - - // Mock questions returned from Canvas in correct order - vi.mocked(paginatedRequest).mockResolvedValueOnce([ - { - id: 1, - quiz_id: 1, - position: 1, - question_name: "Question 1", - question_type: "short_answer_question", - question_text: "First question", - correct_comments: "", - incorrect_comments: "", - neutral_comments: "", - }, - { - id: 2, - quiz_id: 1, - position: 2, - question_name: "Question 2", - question_type: "essay_question", - question_text: "Second question", - correct_comments: "", - incorrect_comments: "", - neutral_comments: "", - }, - ]); - - const result = await canvasQuizService.getQuizQuestions(1, 1); - - // Verify questions are returned in correct order - expect(result).toHaveLength(2); - expect(result[0].position).toBe(1); - expect(result[1].position).toBe(2); - expect(result[0].question_text).toBe("First question"); - expect(result[1].question_text).toBe("Second question"); - }); -}); \ No newline at end of file diff --git a/src/features/local/parsingTests/quizMarkdown/questionFeedback.test.ts b/src/features/local/parsingTests/quizMarkdown/questionFeedback.test.ts new file mode 100644 index 0000000..532f2cf --- /dev/null +++ b/src/features/local/parsingTests/quizMarkdown/questionFeedback.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion"; +import { quizMarkdownUtils } from "@/features/local/quizzes/models/utils/quizMarkdownUtils"; + +describe("Question Feedback options", () => { + it("essay questions can have feedback", () => { + 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 description +... this is general feedback +essay +`; + + const quiz = quizMarkdownUtils.parseMarkdown(rawMarkdownQuiz, name); + const firstQuestion = quiz.questions[0]; + + expect(firstQuestion.questionType).toBe(QuestionType.ESSAY); + expect(firstQuestion.text).not.toContain("this is general feedback"); + expect(firstQuestion.neutralComments).toBe("this is general feedback"); + }); +}); diff --git a/src/features/local/parsingTests/quizMarkdown/quizMarkdown.test.ts b/src/features/local/parsingTests/quizMarkdown/quizMarkdown.test.ts index a99d1e5..425714e 100644 --- a/src/features/local/parsingTests/quizMarkdown/quizMarkdown.test.ts +++ b/src/features/local/parsingTests/quizMarkdown/quizMarkdown.test.ts @@ -5,7 +5,6 @@ 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 = { diff --git a/src/features/local/quizzes/models/utils/quizFeedbackMarkdownUtils.ts b/src/features/local/quizzes/models/utils/quizFeedbackMarkdownUtils.ts new file mode 100644 index 0000000..9dd42bc --- /dev/null +++ b/src/features/local/quizzes/models/utils/quizFeedbackMarkdownUtils.ts @@ -0,0 +1,125 @@ +type FeedbackType = "+" | "-" | "..."; + +const isFeedbackStart = ( + trimmedLine: string, + feedbackType: FeedbackType +): boolean => { + const prefix = feedbackType === "..." ? "... " : `${feedbackType} `; + return trimmedLine.startsWith(prefix) || trimmedLine === feedbackType; +}; + +const extractFeedbackContent = ( + trimmedLine: string, + feedbackType: FeedbackType +): string => { + if (trimmedLine === feedbackType) return ""; + + const prefixLength = feedbackType === "..." ? 4 : 2; // "... " is 4 chars, "+ " and "- " are 2 + return trimmedLine.substring(prefixLength); +}; + +const saveFeedback = ( + feedbackType: FeedbackType | null, + feedbackLines: string[], + comments: { + correct?: string; + incorrect?: string; + neutral?: string; + } +): void => { + if (!feedbackType || feedbackLines.length === 0) return; + + const feedbackText = feedbackLines.join("\n"); + if (feedbackType === "+") { + comments.correct = feedbackText; + } else if (feedbackType === "-") { + comments.incorrect = feedbackText; + } else if (feedbackType === "...") { + comments.neutral = feedbackText; + } +}; + +export const quizFeedbackMarkdownUtils = { + extractFeedback( + linesWithoutPoints: string[], + isAnswerLine: (trimmedLine: string) => boolean + ): { + correctComments?: string; + incorrectComments?: string; + neutralComments?: string; + linesWithoutFeedback: string[]; + } { + const comments: { + correct?: string; + incorrect?: string; + neutral?: string; + } = {}; + const linesWithoutFeedback: string[] = []; + + let currentFeedbackType: FeedbackType | null = null; + let currentFeedbackLines: string[] = []; + + for (const line of linesWithoutPoints) { + const trimmed = line.trim(); + + // Check if this is a new feedback line + let newFeedbackType: FeedbackType | null = null; + if (isFeedbackStart(trimmed, "+")) { + newFeedbackType = "+"; + } else if (isFeedbackStart(trimmed, "-")) { + newFeedbackType = "-"; + } else if (isFeedbackStart(trimmed, "...")) { + newFeedbackType = "..."; + } + + if (newFeedbackType) { + // Save previous feedback if any + saveFeedback(currentFeedbackType, currentFeedbackLines, comments); + + // Start new feedback + currentFeedbackType = newFeedbackType; + const content = extractFeedbackContent(trimmed, newFeedbackType); + currentFeedbackLines = content ? [content] : []; + } else if (currentFeedbackType && !isAnswerLine(trimmed)) { + // This is a continuation of the current feedback + currentFeedbackLines.push(line); + } else { + // Save any pending feedback + saveFeedback(currentFeedbackType, currentFeedbackLines, comments); + currentFeedbackType = null; + currentFeedbackLines = []; + + // This is a regular line + linesWithoutFeedback.push(line); + } + } + + // Save any remaining feedback + saveFeedback(currentFeedbackType, currentFeedbackLines, comments); + + return { + correctComments: comments.correct, + incorrectComments: comments.incorrect, + neutralComments: comments.neutral, + linesWithoutFeedback, + }; + }, + + formatFeedback( + correctComments?: string, + incorrectComments?: string, + neutralComments?: string + ): string { + let feedbackText = ""; + if (correctComments) { + feedbackText += `+ ${correctComments}\n`; + } + if (incorrectComments) { + feedbackText += `- ${incorrectComments}\n`; + } + if (neutralComments) { + feedbackText += `... ${neutralComments}\n`; + } + return feedbackText; + }, +}; diff --git a/src/features/local/quizzes/models/utils/quizQuestionMarkdownUtils.ts b/src/features/local/quizzes/models/utils/quizQuestionMarkdownUtils.ts index c560e86..ac7df36 100644 --- a/src/features/local/quizzes/models/utils/quizQuestionMarkdownUtils.ts +++ b/src/features/local/quizzes/models/utils/quizQuestionMarkdownUtils.ts @@ -1,6 +1,7 @@ import { LocalQuizQuestion, QuestionType } from "../localQuizQuestion"; import { LocalQuizQuestionAnswer } from "../localQuizQuestionAnswer"; import { quizQuestionAnswerMarkdownUtils } from "./quizQuestionAnswerMarkdownUtils"; +import { quizFeedbackMarkdownUtils } from "./quizFeedbackMarkdownUtils"; const _validFirstAnswerDelimiters = [ "*a)", @@ -14,97 +15,11 @@ const _validFirstAnswerDelimiters = [ ]; const _multipleChoicePrefix = ["a)", "*a)", "*)", ")"]; const _multipleAnswerPrefix = ["[ ]", "[*]", "[]"]; -const _feedbackPrefixes = ["+", "-", "..."]; -const extractFeedback = ( - linesWithoutPoints: string[] -): { - correctComments?: string; - incorrectComments?: string; - neutralComments?: string; - linesWithoutFeedback: string[]; -} => { - let correctComments: string | undefined; - let incorrectComments: string | undefined; - let neutralComments: string | undefined; - const linesWithoutFeedback: string[] = []; - - let currentFeedbackType: "+" | "-" | "..." | null = null; - let currentFeedbackLines: string[] = []; - - for (const line of linesWithoutPoints) { - const trimmed = line.trim(); - - // Check if this is a new feedback line - if (trimmed.startsWith("+ ") || trimmed === "+") { - // Save previous feedback if any - if (currentFeedbackType && currentFeedbackLines.length > 0) { - const feedbackText = currentFeedbackLines.join("\n"); - if (currentFeedbackType === "+") correctComments = feedbackText; - else if (currentFeedbackType === "-") incorrectComments = feedbackText; - else if (currentFeedbackType === "...") neutralComments = feedbackText; - } - - currentFeedbackType = "+"; - currentFeedbackLines = trimmed === "+" ? [] : [trimmed.substring(2)]; // Remove "+ " or handle standalone "+" - } else if (trimmed.startsWith("- ") || trimmed === "-") { - // Save previous feedback if any - if (currentFeedbackType && currentFeedbackLines.length > 0) { - const feedbackText = currentFeedbackLines.join("\n"); - if (currentFeedbackType === "+") correctComments = feedbackText; - else if (currentFeedbackType === "-") incorrectComments = feedbackText; - else if (currentFeedbackType === "...") neutralComments = feedbackText; - } - - currentFeedbackType = "-"; - currentFeedbackLines = trimmed === "-" ? [] : [trimmed.substring(2)]; // Remove "- " or handle standalone "-" - } else if (trimmed.startsWith("... ") || trimmed === "...") { - // Save previous feedback if any - if (currentFeedbackType && currentFeedbackLines.length > 0) { - const feedbackText = currentFeedbackLines.join("\n"); - if (currentFeedbackType === "+") correctComments = feedbackText; - else if (currentFeedbackType === "-") incorrectComments = feedbackText; - else if (currentFeedbackType === "...") neutralComments = feedbackText; - } - - currentFeedbackType = "..."; - currentFeedbackLines = trimmed === "..." ? [] : [trimmed.substring(4)]; // Remove "... " or handle standalone "..." - } else if ( - currentFeedbackType && - !_validFirstAnswerDelimiters.some((prefix) => trimmed.startsWith(prefix)) - ) { - // This is a continuation of the current feedback - currentFeedbackLines.push(line); - } else { - // Save any pending feedback - if (currentFeedbackType && currentFeedbackLines.length > 0) { - const feedbackText = currentFeedbackLines.join("\n"); - if (currentFeedbackType === "+") correctComments = feedbackText; - else if (currentFeedbackType === "-") incorrectComments = feedbackText; - else if (currentFeedbackType === "...") neutralComments = feedbackText; - currentFeedbackType = null; - currentFeedbackLines = []; - } - - // This is a regular line - linesWithoutFeedback.push(line); - } - } - - // Save any remaining feedback - if (currentFeedbackType && currentFeedbackLines.length > 0) { - const feedbackText = currentFeedbackLines.join("\n"); - if (currentFeedbackType === "+") correctComments = feedbackText; - else if (currentFeedbackType === "-") incorrectComments = feedbackText; - else if (currentFeedbackType === "...") neutralComments = feedbackText; - } - - return { - correctComments, - incorrectComments, - neutralComments, - linesWithoutFeedback, - }; +const isAnswerLine = (trimmedLine: string): boolean => { + return _validFirstAnswerDelimiters.some((prefix) => + trimmedLine.startsWith(prefix) + ); }; const getAnswerStringsWithMultilineSupport = ( @@ -246,16 +161,11 @@ export const quizQuestionMarkdownUtils = { : ""; // Build feedback lines - let feedbackText = ""; - if (question.correctComments) { - feedbackText += `+ ${question.correctComments}\n`; - } - if (question.incorrectComments) { - feedbackText += `- ${question.incorrectComments}\n`; - } - if (question.neutralComments) { - feedbackText += `... ${question.neutralComments}\n`; - } + const feedbackText = quizFeedbackMarkdownUtils.formatFeedback( + question.correctComments, + question.incorrectComments, + question.neutralComments + ); const answersText = answerArray.join("\n"); const questionTypeIndicator = @@ -292,7 +202,10 @@ export const quizQuestionMarkdownUtils = { incorrectComments, neutralComments, linesWithoutFeedback, - } = extractFeedback(linesWithoutPoints); + } = quizFeedbackMarkdownUtils.extractFeedback( + linesWithoutPoints, + isAnswerLine + ); const { linesWithoutAnswers } = linesWithoutFeedback.reduce( ({ linesWithoutAnswers, taking }, currentLine) => {