diff --git a/.changeset/nine-roses-enjoy.md b/.changeset/nine-roses-enjoy.md new file mode 100644 index 0000000000..aee515d2e0 --- /dev/null +++ b/.changeset/nine-roses-enjoy.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Move validation logic out of the iframe widget and add tests diff --git a/packages/perseus/src/widgets/iframe/iframe-validator.test.ts b/packages/perseus/src/widgets/iframe/iframe-validator.test.ts new file mode 100644 index 0000000000..1c20a891c2 --- /dev/null +++ b/packages/perseus/src/widgets/iframe/iframe-validator.test.ts @@ -0,0 +1,49 @@ +import {iframeValidator} from "./iframe-validator"; + +import type {PerseusIFrameUserInput} from "../../validation.types"; + +describe("iframeValidator", () => { + it("is correct when the state from the iframe shows the status is correct", () => { + // Arrange + const state: PerseusIFrameUserInput = { + status: "correct", + message: "Good job!", + }; + + // Act + const result = iframeValidator(state); + + // Assert + expect(result).toHaveBeenAnsweredCorrectly(); + }); + + it("is incorrect when the state from the iframe shows the status is incorrect", () => { + // Arrange + const state: PerseusIFrameUserInput = { + status: "incorrect", + message: "Try again!", + }; + + // Act + const result = iframeValidator(state); + + // Assert + expect(result).toHaveBeenAnsweredIncorrectly(); + }); + + // Note: It looks like the iframe only says if the answer is correct or + // incorrect, but status is set to "incomplete" by default. + it("should return invalid score before user interactions", () => { + // Arrange + const state: PerseusIFrameUserInput = { + status: "incomplete", + message: null, + }; + + // Act + const result = iframeValidator(state); + + // Assert + expect(result).toHaveInvalidInput("Keep going, you're not there yet!"); + }); +}); diff --git a/packages/perseus/src/widgets/iframe/iframe-validator.ts b/packages/perseus/src/widgets/iframe/iframe-validator.ts new file mode 100644 index 0000000000..6dcdf8088b --- /dev/null +++ b/packages/perseus/src/widgets/iframe/iframe-validator.ts @@ -0,0 +1,27 @@ +import type {PerseusScore} from "../../types"; +import type {PerseusIFrameUserInput} from "../../validation.types"; + +export function iframeValidator(state: PerseusIFrameUserInput): PerseusScore { + // The iframe can tell us whether it's correct or incorrect, + // and pass an optional message + if (state.status === "correct") { + return { + type: "points", + earned: 1, + total: 1, + message: state.message || null, + }; + } + if (state.status === "incorrect") { + return { + type: "points", + earned: 0, + total: 1, + message: state.message || null, + }; + } + return { + type: "invalid", + message: "Keep going, you're not there yet!", + }; +} diff --git a/packages/perseus/src/widgets/iframe/iframe.tsx b/packages/perseus/src/widgets/iframe/iframe.tsx index a44936eb93..7f9883d950 100644 --- a/packages/perseus/src/widgets/iframe/iframe.tsx +++ b/packages/perseus/src/widgets/iframe/iframe.tsx @@ -15,8 +15,10 @@ import {getDependencies} from "../../dependencies"; import * as Changeable from "../../mixins/changeable"; import Util from "../../util"; +import {iframeValidator} from "./iframe-validator"; + import type {PerseusIFrameWidgetOptions} from "../../perseus-types"; -import type {WidgetExports, WidgetProps} from "../../types"; +import type {PerseusScore, WidgetExports, WidgetProps} from "../../types"; import type { PerseusIFrameRubric, PerseusIFrameUserInput, @@ -51,6 +53,10 @@ class Iframe extends React.Component { allowTopNavigation: false, }; + static validate(state: PerseusIFrameUserInput): PerseusScore { + return iframeValidator(state); + } + componentDidMount() { $(window).on("message", this.handleMessageEvent); } @@ -90,10 +96,9 @@ class Iframe extends React.Component { return Changeable.change.apply(this, args); }; - simpleValidate: (arg1: any) => any = (rubric) => { - // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Iframe'. - return Iframe.validate(this.getUserInput(), rubric); - }; + simpleValidate(): PerseusScore { + return iframeValidator(this.getUserInput()); + } render(): React.ReactNode { const style = { @@ -163,36 +168,6 @@ class Iframe extends React.Component { } } -/** - * This is the widget's grading function - */ -_.extend(Iframe, { - validate: function (state, rubric) { - // The iframe can tell us whether it's correct or incorrect, - // and pass an optional message - if (state.status === "correct") { - return { - type: "points", - earned: 1, - total: 1, - message: state.message || null, - }; - } - if (state.status === "incorrect") { - return { - type: "points", - earned: 0, - total: 1, - message: state.message || null, - }; - } - return { - type: "invalid", - message: "Keep going, you're not there yet!", - }; - }, -}); - export default { name: "iframe", displayName: "Iframe (deprecated)",