diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ActiveCodeAnswerView/ActiveCodeAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ActiveCodeAnswerView/spec/ActiveCodeAnswerView.spec.tsx similarity index 82% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ActiveCodeAnswerView/ActiveCodeAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ActiveCodeAnswerView/spec/ActiveCodeAnswerView.spec.tsx index 50b330b10..0b8509dd0 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ActiveCodeAnswerView/ActiveCodeAnswerView.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ActiveCodeAnswerView/spec/ActiveCodeAnswerView.spec.tsx @@ -1,10 +1,7 @@ -import { describe, expect, it } from "vitest"; - import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; - -import { ActiveCodeAnswerView } from "./ActiveCodeAnswerView"; +import { AnswerRendererProps } from "../../types"; +import { ActiveCodeAnswerView } from "../ActiveCodeAnswerView"; const baseProps = (answer: string): AnswerRendererProps => ({ answer, diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AnswerRenderer/AnswerRenderer.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AnswerRenderer/spec/AnswerRenderer.spec.tsx similarity index 85% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AnswerRenderer/AnswerRenderer.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AnswerRenderer/spec/AnswerRenderer.spec.tsx index 75281acca..bdac255c2 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AnswerRenderer/AnswerRenderer.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AnswerRenderer/spec/AnswerRenderer.spec.tsx @@ -1,25 +1,23 @@ import { GraderAnswerHistoryItem } from "@store/grader/grader.logic.api"; -import { describe, expect, it, vi } from "vitest"; import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; +import { AnswerRendererProps } from "../../types"; +import { AnswerRenderer } from "../AnswerRenderer"; -import { AnswerRenderer } from "./AnswerRenderer"; - -vi.mock("../McqAnswerView", () => ({ McqAnswerView: () =>
VIEW:MCQ
})); -vi.mock("../AssociationAnswerView", () => ({ +vi.mock("../../McqAnswerView", () => ({ McqAnswerView: () =>
VIEW:MCQ
})); +vi.mock("../../AssociationAnswerView", () => ({ AssociationAnswerView: ({ kind }: { kind: string }) =>
VIEW:{kind.toUpperCase()}
})); -vi.mock("../FitbAnswerView", () => ({ FitbAnswerView: () =>
VIEW:FITB
})); -vi.mock("../ShortAnswerView", () => ({ ShortAnswerView: () =>
VIEW:SHORT
})); -vi.mock("../ParsonsAnswerView", () => ({ ParsonsAnswerView: () =>
VIEW:PARSONS
})); -vi.mock("../ActiveCodeAnswerView", () => ({ +vi.mock("../../FitbAnswerView", () => ({ FitbAnswerView: () =>
VIEW:FITB
})); +vi.mock("../../ShortAnswerView", () => ({ ShortAnswerView: () =>
VIEW:SHORT
})); +vi.mock("../../ParsonsAnswerView", () => ({ ParsonsAnswerView: () =>
VIEW:PARSONS
})); +vi.mock("../../ActiveCodeAnswerView", () => ({ ActiveCodeAnswerView: () =>
VIEW:ACTIVECODE
})); -vi.mock("../DefaultAnswerView", () => ({ DefaultAnswerView: () =>
VIEW:DEFAULT
})); -vi.mock("../IframeAnswerView", () => ({ IframeAnswerView: () =>
VIEW:IFRAME
})); -vi.mock("../RunestoneGraderPreview", () => ({ +vi.mock("../../DefaultAnswerView", () => ({ DefaultAnswerView: () =>
VIEW:DEFAULT
})); +vi.mock("../../IframeAnswerView", () => ({ IframeAnswerView: () =>
VIEW:IFRAME
})); +vi.mock("../../RunestoneGraderPreview", () => ({ RunestoneGraderPreview: ({ attemptId }: { attemptId?: string | number }) => (
PREVIEW:{String(attemptId)}
) diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/AssociationAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/spec/AssociationAnswerView.spec.tsx similarity index 91% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/AssociationAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/spec/AssociationAnswerView.spec.tsx index 5b6706adc..f60b938d9 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/AssociationAnswerView.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/spec/AssociationAnswerView.spec.tsx @@ -1,11 +1,8 @@ -import { describe, expect, it } from "vitest"; - import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; - -import { AssociationAnswerView } from "./AssociationAnswerView"; -import { formatAssociationAnswer, parseAssociations } from "./utils"; +import { AnswerRendererProps } from "../../types"; +import { AssociationAnswerView } from "../AssociationAnswerView"; +import { formatAssociationAnswer, parseAssociations } from "../utils"; const baseProps = (overrides: Partial = {}): AnswerRendererProps => ({ answer: "", diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/formatAssociationAnswer.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/formatAssociationAnswer.spec.ts new file mode 100644 index 000000000..0be342a7a --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/formatAssociationAnswer.spec.ts @@ -0,0 +1,13 @@ +import { formatAssociationAnswer } from "../formatAssociationAnswer"; + +describe("formatAssociationAnswer", () => { + it("falls back to ids when the question has no matching label", () => { + expect( + formatAssociationAnswer( + "matching", + '{"connections":[{"from":"known","to":"unknown"}]}', + 'Known label' + ) + ).toBe("Known label → unknown"); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseAssociationLabels.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseAssociationLabels.spec.ts new file mode 100644 index 000000000..77bd6252c --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseAssociationLabels.spec.ts @@ -0,0 +1,19 @@ +import { parseAssociationLabels } from "../parseAssociationLabels"; + +describe("parseAssociationLabels", () => { + it("extracts plain labels from JSON and legacy draggable markup", () => { + const htmlsrc = `
+ + Legacy item +
`; + + expect(parseAssociationLabels(htmlsrc)).toEqual({ + "left-1": "Loop", + "right-1": "Repeats statements", + "legacy-item": "Legacy item" + }); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseAssociations.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseAssociations.spec.ts new file mode 100644 index 000000000..52680ce01 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseAssociations.spec.ts @@ -0,0 +1,20 @@ +import { parseAssociations } from "../parseAssociations"; + +describe("parseAssociations", () => { + it("keeps only valid matching connections", () => { + const answer = JSON.stringify({ + connections: [{ from: "left-1", to: "right-1" }, { from: "left-2" }, null, "invalid"] + }); + + expect(parseAssociations("matching", answer)).toEqual([{ from: "left-1", to: "right-1" }]); + }); + + it("flattens drag-and-drop zones and ignores invalid values", () => { + expect( + parseAssociations("dragndrop", '{"zone-1":["item-1",5,"item-2"],"zone-2":null}') + ).toEqual([ + { from: "item-1", to: "zone-1" }, + { from: "item-2", to: "zone-1" } + ]); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseJsonObject.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseJsonObject.spec.ts new file mode 100644 index 000000000..dee25f743 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/parseJsonObject.spec.ts @@ -0,0 +1,11 @@ +import { parseJsonObject } from "../parseJsonObject"; + +describe("parseJsonObject", () => { + it("accepts JSON objects", () => { + expect(parseJsonObject('{"value":1}')).toEqual({ value: 1 }); + }); + + it.each(["[]", "null", "not-json"])("rejects invalid or non-object JSON: %s", (value) => { + expect(parseJsonObject(value)).toBeNull(); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/textOnly.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/textOnly.spec.ts new file mode 100644 index 000000000..04517e165 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/AssociationAnswerView/utils/spec/textOnly.spec.ts @@ -0,0 +1,11 @@ +import { textOnly } from "../textOnly"; + +describe("textOnly", () => { + it("removes markup and trims text", () => { + expect(textOnly(" Loop ")).toBe("Loop"); + }); + + it("returns an empty string for non-string values", () => { + expect(textOnly(42)).toBe(""); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/spec/CompactAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/spec/CompactAnswerView.spec.tsx similarity index 100% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/spec/CompactAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/spec/CompactAnswerView.spec.tsx diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/utils/spec/formatCompactAnswer.test.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/utils/spec/formatCompactAnswer.spec.ts similarity index 100% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/utils/spec/formatCompactAnswer.test.ts rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/CompactAnswerView/utils/spec/formatCompactAnswer.spec.ts diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/DefaultAnswerView/DefaultAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/DefaultAnswerView/spec/DefaultAnswerView.spec.tsx similarity index 82% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/DefaultAnswerView/DefaultAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/DefaultAnswerView/spec/DefaultAnswerView.spec.tsx index 51517faf4..a10984c00 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/DefaultAnswerView/DefaultAnswerView.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/DefaultAnswerView/spec/DefaultAnswerView.spec.tsx @@ -1,10 +1,7 @@ -import { describe, expect, it } from "vitest"; - import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; - -import { DefaultAnswerView } from "./DefaultAnswerView"; +import { AnswerRendererProps } from "../../types"; +import { DefaultAnswerView } from "../DefaultAnswerView"; const baseProps = (answer: string): AnswerRendererProps => ({ answer, diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/FitbAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/spec/FitbAnswerView.spec.tsx similarity index 90% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/FitbAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/spec/FitbAnswerView.spec.tsx index 038bc4b59..9d64c87da 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/FitbAnswerView.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/spec/FitbAnswerView.spec.tsx @@ -1,10 +1,7 @@ -import { describe, expect, it } from "vitest"; - import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; - -import { FitbAnswerView } from "./FitbAnswerView"; +import { AnswerRendererProps } from "../../types"; +import { FitbAnswerView } from "../FitbAnswerView"; const baseProps = (answer: string): AnswerRendererProps => ({ answer, diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/utils/spec/parseBlankValues.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/utils/spec/parseBlankValues.spec.ts new file mode 100644 index 000000000..6b4f31700 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/FitbAnswerView/utils/spec/parseBlankValues.spec.ts @@ -0,0 +1,14 @@ +import { parseBlankValues } from "../parseBlankValues"; + +describe("parseBlankValues", () => { + it.each([ + ['["first","second"]', ["first", "second"]], + ["5", ["5"]], + ["true", ["true"]], + ['{"value":1}', ["[object Object]"]], + ["first,second", ["first", "second"]], + ["", []] + ])("parses %s", (answer, expected) => { + expect(parseBlankValues(answer)).toEqual(expected); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/IframeAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/spec/IframeAnswerView.spec.tsx similarity index 96% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/IframeAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/spec/IframeAnswerView.spec.tsx index d38e92ed0..e630cc16e 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/IframeAnswerView.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/spec/IframeAnswerView.spec.tsx @@ -1,12 +1,10 @@ import { GraderAnswerHistoryItem } from "@store/grader/grader.logic.api"; -import { describe, expect, it } from "vitest"; import { renderWithMantine, screen, waitFor } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; - -import { IframeAnswerView } from "./IframeAnswerView"; -import { extractFrameSpec } from "./utils"; +import { AnswerRendererProps } from "../../types"; +import { IframeAnswerView } from "../IframeAnswerView"; +import { extractFrameSpec } from "../utils"; const DOENET_HTMLSRC = `
diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/extractFrameSpec.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/extractFrameSpec.spec.ts new file mode 100644 index 000000000..eb9d76dea --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/extractFrameSpec.spec.ts @@ -0,0 +1,15 @@ +import { extractFrameSpec } from "../extractFrameSpec"; + +describe("extractFrameSpec", () => { + it("extracts iframe attributes", () => { + expect(extractFrameSpec('')).toEqual({ + src: "/activity", + style: "height: 20px" + }); + }); + + it("rejects incomplete markup", () => { + expect(extractFrameSpec("")).toBeNull(); + expect(extractFrameSpec()).toBeNull(); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/formatState.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/formatState.spec.ts new file mode 100644 index 000000000..75891ce46 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/formatState.spec.ts @@ -0,0 +1,16 @@ +import { formatState } from "../formatState"; + +describe("formatState", () => { + it("formats empty, string, and object saved state", () => { + expect(formatState(null)).toBe("(no state recorded)"); + expect(formatState("saved")).toBe("saved"); + expect(formatState({ value: 1 })).toBe('{\n "value": 1\n}'); + }); + + it("falls back to string conversion for circular state", () => { + const circular: Record = {}; + + circular.self = circular; + expect(formatState(circular)).toBe("[object Object]"); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/getSpliceWrapper.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/getSpliceWrapper.spec.ts new file mode 100644 index 000000000..e7e15891e --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/getSpliceWrapper.spec.ts @@ -0,0 +1,20 @@ +import { getSpliceWrapper } from "../getSpliceWrapper"; + +describe("getSpliceWrapper", () => { + afterEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any).spliceWrapper; + }); + + it("returns an already loaded SPLICE wrapper", () => { + const wrapper = { + registerGraderFrame: vi.fn(), + unregisterGraderFrame: vi.fn() + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).spliceWrapper = wrapper; + + expect(getSpliceWrapper()).toBe(wrapper); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/waitForSpliceWrapper.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/waitForSpliceWrapper.spec.ts new file mode 100644 index 000000000..5b0a364cb --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/IframeAnswerView/utils/spec/waitForSpliceWrapper.spec.ts @@ -0,0 +1,37 @@ +import { WRAPPER_POLL_MS } from "../../constants"; +import { waitForSpliceWrapper } from "../waitForSpliceWrapper"; + +describe("waitForSpliceWrapper", () => { + afterEach(() => { + vi.useRealTimers(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any).spliceWrapper; + }); + + it("returns an already loaded SPLICE wrapper immediately", async () => { + const wrapper = { + registerGraderFrame: vi.fn(), + unregisterGraderFrame: vi.fn() + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).spliceWrapper = wrapper; + + await expect(waitForSpliceWrapper()).resolves.toBe(wrapper); + }); + + it("waits until the SPLICE wrapper becomes available", async () => { + vi.useFakeTimers(); + const wrapper = { + registerGraderFrame: vi.fn(), + unregisterGraderFrame: vi.fn() + }; + const waiting = waitForSpliceWrapper(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).spliceWrapper = wrapper; + await vi.advanceTimersByTimeAsync(WRAPPER_POLL_MS); + + await expect(waiting).resolves.toBe(wrapper); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/McqAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/spec/McqAnswerView.spec.tsx similarity index 93% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/McqAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/spec/McqAnswerView.spec.tsx index 7a2e05208..e94be8f68 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/McqAnswerView.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/spec/McqAnswerView.spec.tsx @@ -1,10 +1,7 @@ -import { describe, expect, it } from "vitest"; - import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; - -import { McqAnswerView } from "./McqAnswerView"; +import { AnswerRendererProps } from "../../types"; +import { McqAnswerView } from "../McqAnswerView"; const baseProps = (overrides: Partial = {}): AnswerRendererProps => ({ answer: "", history: [], diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/utils/spec/optionIndexToLabel.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/utils/spec/optionIndexToLabel.spec.ts new file mode 100644 index 000000000..d75016c4d --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/utils/spec/optionIndexToLabel.spec.ts @@ -0,0 +1,19 @@ +import { optionIndexToLabel } from "../optionIndexToLabel"; + +describe("optionIndexToLabel", () => { + it.each([ + ["0", "A"], + ["25", "Z"], + ["26", "AA"], + ["27", "AB"], + ["51", "AZ"], + ["52", "BA"] + ])("converts option index %s to %s", (index, label) => { + expect(optionIndexToLabel(index)).toBe(label); + }); + + it("preserves legacy and unsafe option values", () => { + expect(optionIndexToLabel("legacy-a")).toBe("legacy-a"); + expect(optionIndexToLabel("9007199254740992")).toBe("9007199254740992"); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/utils/spec/parseSelectedOptions.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/utils/spec/parseSelectedOptions.spec.ts new file mode 100644 index 000000000..44091a8c6 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/McqAnswerView/utils/spec/parseSelectedOptions.spec.ts @@ -0,0 +1,8 @@ +import { parseSelectedOptions } from "../parseSelectedOptions"; + +describe("parseSelectedOptions", () => { + it("splits selected options and removes empty segments", () => { + expect(parseSelectedOptions("0,,2")).toEqual(["0", "2"]); + expect(parseSelectedOptions("")).toEqual([]); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/spec/ParsonsAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/spec/ParsonsAnswerView.spec.tsx similarity index 100% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/spec/ParsonsAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/spec/ParsonsAnswerView.spec.tsx diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/extractParsonsSource.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/extractParsonsSource.spec.ts new file mode 100644 index 000000000..82fc3c3db --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/extractParsonsSource.spec.ts @@ -0,0 +1,69 @@ +import { extractParsonsSource } from "../extractParsonsSource"; + +describe("extractParsonsSource", () => { + it("extracts decoded lines, indentation levels, and removes Parsons metadata", () => { + const htmlsrc = `
+if left < right:
+---
+    print(left) #distractor: not needed
+---
+        return left
+    
`; + + expect(extractParsonsSource(htmlsrc)).toEqual({ + indentUnit: " ", + lines: [ + { indentLevel: 0, text: "if left < right:" }, + { indentLevel: 1, text: "print(left)" }, + { indentLevel: 2, text: "return left" } + ], + noIndent: false + }); + }); + + it("recognizes questions where indentation cannot be changed", () => { + expect( + extractParsonsSource('
a\n---\n  b
') + ).toMatchObject({ indentUnit: " ", noIndent: true }); + }); + + it.each([ + ["line #paired:alternative", "line"], + ["line #tag:condition;depends:setup;", "line"] + ])("removes supported block metadata from %j", (block, expected) => { + expect(extractParsonsSource(`
${block}
`)).toMatchObject({ + lines: [{ indentLevel: 0, text: expected }] + }); + }); + + it("parses line-based sources without block separators", () => { + expect(extractParsonsSource('
first\n  second
')).toMatchObject({ + indentUnit: " ", + lines: [ + { indentLevel: 0, text: "first" }, + { indentLevel: 1, text: "second" } + ] + }); + }); + + it("uses the default indentation when the source has no indented lines", () => { + expect(extractParsonsSource('
first\nsecond
')).toMatchObject({ + indentUnit: " " + }); + }); + + it("recognizes tabs as the indentation unit", () => { + expect( + extractParsonsSource('
first\n---\n\tsecond
') + ).toMatchObject({ + indentUnit: "\t" + }); + }); + + it.each([undefined, "", "
Question only
"])( + "returns null when the Parsons source is absent from %j", + (source) => { + expect(extractParsonsSource(source)).toBeNull(); + } + ); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/extractParsonsSource.test.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/extractParsonsSource.test.ts deleted file mode 100644 index ca4ad6f82..000000000 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/extractParsonsSource.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { extractParsonsSource } from "../extractParsonsSource"; - -describe("extractParsonsSource", () => { - it("extracts decoded lines, indentation levels, and removes Parsons metadata", () => { - const htmlsrc = `
-if left < right:
----
-    print(left) #distractor: not needed
----
-        return left
-    
`; - - expect(extractParsonsSource(htmlsrc)).toEqual({ - indentUnit: " ", - lines: [ - { indentLevel: 0, text: "if left < right:" }, - { indentLevel: 1, text: "print(left)" }, - { indentLevel: 2, text: "return left" } - ], - noIndent: false - }); - }); - - it("recognizes questions where indentation cannot be changed", () => { - expect( - extractParsonsSource('
a\n---\n  b
') - ).toMatchObject({ indentUnit: " ", noIndent: true }); - }); - - it("returns null when the Parsons source is absent", () => { - expect(extractParsonsSource("
Question only
")).toBeNull(); - }); -}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/parseParsonsHash.test.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/parseParsonsHash.spec.ts similarity index 76% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/parseParsonsHash.test.ts rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/parseParsonsHash.spec.ts index 24ae6712d..4efe45205 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/parseParsonsHash.test.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/parseParsonsHash.spec.ts @@ -8,6 +8,13 @@ describe("parseParsonsHash", () => { ]); }); + it("ignores surrounding whitespace", () => { + expect(parseParsonsHash(" 0_1_0-2_2 ")).toEqual([ + { lineIndexes: [0, 1], indent: 0 }, + { lineIndexes: [2], indent: 2 } + ]); + }); + it.each(["", "-", null, undefined])("treats %j as an empty answer", (answer) => { expect(parseParsonsHash(answer)).toEqual([]); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/reconstructParsonsAnswer.test.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/reconstructParsonsAnswer.spec.ts similarity index 60% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/reconstructParsonsAnswer.test.ts rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/reconstructParsonsAnswer.spec.ts index 6998d0ee4..66b5c97bd 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/reconstructParsonsAnswer.test.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ParsonsAnswerView/utils/spec/reconstructParsonsAnswer.spec.ts @@ -29,16 +29,45 @@ describe("reconstructParsonsAnswer", () => { ); }); + it("preserves source indentation when indentation changes are disabled", () => { + const noIndentSource = + '
first\n---\n  second
'; + + expect(reconstructParsonsAnswer("0_4-1_0", noIndentSource)).toBe("first\n second"); + }); + + it("preserves blank lines inside a multi-line block", () => { + const sourceWithBlankLine = `
+if ready:
+---
+    start()
+
+    finish()
+    
`; + + expect(reconstructParsonsAnswer("0_1_2_3_0", sourceWithBlankLine)).toBe( + "if ready:\n start()\n\n finish()" + ); + }); + it("formats legacy plain-text answers without block separators", () => { expect(reconstructParsonsAnswer("def greet(name):\n---\n if name:")).toBe( "def greet(name):\n if name:" ); }); + it("normalizes legacy line endings and padded block separators", () => { + expect(reconstructParsonsAnswer("first\r\n \t--- \r\n second\r")).toBe("first\n second"); + }); + it("returns null for a hash when its question source is unavailable", () => { expect(reconstructParsonsAnswer("0_0")).toBeNull(); }); + it("returns null when a hash references a missing source line", () => { + expect(reconstructParsonsAnswer("99_0", htmlsrc)).toBeNull(); + }); + it.each([null, undefined])("treats a %s answer as empty", (answer) => { expect(reconstructParsonsAnswer(answer, htmlsrc)).toBe(""); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/utils/spec/answerForRestore.spec.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/utils/spec/answerForRestore.spec.ts new file mode 100644 index 000000000..4e93f5ad2 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/utils/spec/answerForRestore.spec.ts @@ -0,0 +1,15 @@ +import { answerForRestore } from "../answerForRestore"; + +describe("answerForRestore", () => { + it("parses legacy string matching answers before calling restoreAnswers", () => { + expect(answerForRestore("matching", '{"connections":[{"from":"left","to":"right"}]}')).toEqual({ + connections: [{ from: "left", to: "right" }] + }); + }); + + it("keeps drag-and-drop JSON serialized because its component parses the string", () => { + const answer = '{"zone":["item"]}'; + + expect(answerForRestore("dragndrop", answer)).toBe(answer); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/RunestoneGraderPreview.test.ts b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/utils/spec/restoreDataForAttempt.spec.ts similarity index 53% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/RunestoneGraderPreview.test.ts rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/utils/spec/restoreDataForAttempt.spec.ts index aae7814d9..49b90e8af 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/RunestoneGraderPreview.test.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestoneGraderPreview/utils/spec/restoreDataForAttempt.spec.ts @@ -1,20 +1,8 @@ import { GraderAnswerHistoryItem } from "@store/grader/grader.logic.api"; -import { describe, expect, it } from "vitest"; -import { answerForRestore, restoreDataForAttempt } from "./utils"; -describe("RunestoneGraderPreview restore data", () => { - it("parses legacy string matching answers before calling restoreAnswers", () => { - expect(answerForRestore("matching", '{"connections":[{"from":"left","to":"right"}]}')).toEqual({ - connections: [{ from: "left", to: "right" }] - }); - }); - - it("keeps drag-and-drop JSON serialized because its component parses the string", () => { - const answer = '{"zone":["item"]}'; - - expect(answerForRestore("dragndrop", answer)).toBe(answer); - }); +import { restoreDataForAttempt } from "../restoreDataForAttempt"; +describe("restoreDataForAttempt", () => { it("normalizes a null stored answer to the empty value expected by Runestone components", () => { expect(restoreDataForAttempt("mchoice", { id: 2, answer: null }, "student-1").answer).toBe(""); }); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestonePreview/spec/RunestonePreview.spec.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestonePreview/spec/RunestonePreview.spec.tsx new file mode 100644 index 000000000..4d62b42f8 --- /dev/null +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/RunestonePreview/spec/RunestonePreview.spec.tsx @@ -0,0 +1,55 @@ +import { renderRunestoneComponent } from "@/componentFuncs"; +import { renderWithMantine, screen, waitFor } from "@/test/renderWithMantine"; + +import { QuestionPreviewHeader, RunestonePreview } from "../RunestonePreview"; + +vi.mock("@/componentFuncs", () => ({ + renderRunestoneComponent: vi.fn(() => Promise.resolve()) +})); + +vi.mock("@components/routes/AssignmentBuilder/MathJaxWrapper", () => ({ + MathJaxWrapper: ({ children }: { children: React.ReactNode }) => <>{children} +})); + +vi.mock("better-react-mathjax", () => ({ + MathJax: ({ children }: { children: React.ReactNode }) => <>{children} +})); + +const renderRunestoneComponentMock = vi.mocked(renderRunestoneComponent); + +describe("RunestonePreview", () => { + beforeEach(() => { + renderRunestoneComponentMock.mockClear(); + }); + + it("shows an explanatory message when rendered question HTML is unavailable", () => { + renderWithMantine(); + + expect(screen.getByText("No rendered question preview available.")).toBeInTheDocument(); + expect(renderRunestoneComponentMock).not.toHaveBeenCalled(); + }); + + it("injects the question markup and initializes the Runestone component", async () => { + const { container } = renderWithMantine( + + ); + + expect(screen.getByTestId("rendered-question")).toHaveTextContent("Question body"); + await waitFor(() => expect(renderRunestoneComponentMock).toHaveBeenCalledTimes(1)); + + const previewElement = container.querySelector(".ptx-runestone-container > div > div"); + const [previewRef, options] = renderRunestoneComponentMock.mock.calls[0]; + + expect(previewRef.current).toBe(previewElement); + expect(options).toEqual({ isCalledFromBuilder: true, graderactive: false }); + }); + + it("renders the question name in the shared preview header", () => { + renderWithMantine(); + + expect(screen.getByRole("heading", { name: "Question: parsons-greeting" })).toBeInTheDocument(); + }); +}); diff --git a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ShortAnswerView/ShortAnswerView.test.tsx b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ShortAnswerView/spec/ShortAnswerView.spec.tsx similarity index 83% rename from bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ShortAnswerView/ShortAnswerView.test.tsx rename to bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ShortAnswerView/spec/ShortAnswerView.spec.tsx index 70949c351..72c16a566 100644 --- a/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ShortAnswerView/ShortAnswerView.test.tsx +++ b/bases/rsptx/assignment_server_api/assignment_builder/src/components/routes/Grader/components/questionTypes/ShortAnswerView/spec/ShortAnswerView.spec.tsx @@ -1,10 +1,7 @@ -import { describe, expect, it } from "vitest"; - import { renderWithMantine, screen } from "@/test/renderWithMantine"; -import { AnswerRendererProps } from "../types"; - -import { ShortAnswerView } from "./ShortAnswerView"; +import { AnswerRendererProps } from "../../types"; +import { ShortAnswerView } from "../ShortAnswerView"; const baseProps = (answer: string): AnswerRendererProps => ({ answer, history: [], diff --git a/bases/rsptx/assignment_server_api/assignment_builder/tsconfig.json b/bases/rsptx/assignment_server_api/assignment_builder/tsconfig.json index f9a4ca13b..0aed58e2d 100755 --- a/bases/rsptx/assignment_server_api/assignment_builder/tsconfig.json +++ b/bases/rsptx/assignment_server_api/assignment_builder/tsconfig.json @@ -31,5 +31,5 @@ "vite.config.ts", "playwright.config.ts" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "coverage", "e2e/playwright-report", "e2e/test-results"] } diff --git a/bases/rsptx/assignment_server_api/assignment_builder/vitest.setup.ts b/bases/rsptx/assignment_server_api/assignment_builder/vitest.setup.ts index 7c6522ca3..1962a60dd 100755 --- a/bases/rsptx/assignment_server_api/assignment_builder/vitest.setup.ts +++ b/bases/rsptx/assignment_server_api/assignment_builder/vitest.setup.ts @@ -26,6 +26,7 @@ const installStorage = () => { const missing = (["localStorage", "sessionStorage"] as const).filter( (name) => typeof (globalThis as Record)[name] === "undefined" ); + if (missing.length === 0) { return; } @@ -62,7 +63,9 @@ if (typeof window !== "undefined") { if (!("ResizeObserver" in window)) { class ResizeObserverMock { observe() {} + unobserve() {} + disconnect() {} } (window as unknown as { ResizeObserver: unknown }).ResizeObserver = ResizeObserverMock;