Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/two-tomatoes-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": patch
"@khanacademy/perseus-editor": patch
---

Add expression widget warnings to new save warning utility. Not ready for use yet.
151 changes: 149 additions & 2 deletions packages/perseus-core/src/widgets/expression/expression-util.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import getExpressionPublicWidgetOptions from "./expression-util";
import {registerCoreWidgets} from "../core-widget-registry";

import type {PerseusExpressionWidgetOptions} from "../../data-schema";
import getExpressionPublicWidgetOptions, {
getSaveWarningsForExpressionWidget,
} from "./expression-util";

import type {
ExpressionWidget,
PerseusExpressionWidgetOptions,
} from "../../data-schema";

function getExpressionWidgetWithOptions(
options: Partial<PerseusExpressionWidgetOptions>,
): ExpressionWidget {
return {
type: "expression",
options: {
answerForms: [],
buttonSets: ["basic"],
functions: [],
times: false,
...options,
},
};
}

describe("getExpressionPublicWidgetOptions", () => {
it("should return the correct public options without any answer data", () => {
Expand Down Expand Up @@ -38,3 +60,128 @@ describe("getExpressionPublicWidgetOptions", () => {
});
});
});

describe("getSaveWarningsForExpressionWidget", () => {
beforeAll(() => {
registerCoreWidgets();
});

it("returns a warning when no answers are specified", () => {
// Arrange
const widget = getExpressionWidgetWithOptions({
answerForms: [],
});

// Act
const warnings = getSaveWarningsForExpressionWidget(widget);

// Assert
expect(warnings).toEqual(["No answers specified"]);
});

it("returns a warning when no correct answer is specified", () => {
// Arrange
const widget = getExpressionWidgetWithOptions({
answerForms: [
{
value: "1",
form: false,
simplify: false,
considered: "wrong",
},
],
});

// Act
const warnings = getSaveWarningsForExpressionWidget(widget);

// Assert
expect(warnings).toEqual(["No correct answer specified"]);
});

it("returns a warning when an answer is empty", () => {
// Arrange
const widget = getExpressionWidgetWithOptions({
answerForms: [
{
value: "a",
form: false,
simplify: false,
considered: "correct",
},
{
value: "",
form: false,
simplify: false,
considered: "correct",
},
],
});

// Act
const warnings = getSaveWarningsForExpressionWidget(widget);

// Assert
expect(warnings).toEqual(["Answer 2 is empty"]);
});

it("returns a warning when value could not be parsed", () => {
// Arrange
const widget = getExpressionWidgetWithOptions({
answerForms: [
{
value: "2.4.r",
form: false,
simplify: false,
considered: "correct",
},
],
});
// Act
const warnings = getSaveWarningsForExpressionWidget(widget);

// Assert
expect(warnings).toEqual(["Couldn't parse 2.4.r"]);
});

it("returns a warning if value is not simplified but is required to be", () => {
// Arrange
const widget = getExpressionWidgetWithOptions({
answerForms: [
{
value: "2/1",
form: false,
simplify: true,
considered: "correct",
},
],
});
// Act
const warnings = getSaveWarningsForExpressionWidget(widget);

// Assert
expect(warnings).toEqual([
"2/1 isn't simplified, but is required to be",
]);
});

it("returns an empty array when no warnings are detected", () => {
// Arrange
const widget = getExpressionWidgetWithOptions({
answerForms: [
{
value: "2",
form: false,
simplify: false,
considered: "correct",
},
],
});

// Act
const warnings = getSaveWarningsForExpressionWidget(widget);

// Assert
expect(warnings).toEqual([]);
});
});
51 changes: 50 additions & 1 deletion packages/perseus-core/src/widgets/expression/expression-util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type {PerseusExpressionWidgetOptions} from "../../data-schema";
import * as KAS from "@khanacademy/kas";

import type {
ExpressionWidget,
PerseusExpressionWidgetOptions,
} from "../../data-schema";

/**
* For details on the individual options, see the
Expand Down Expand Up @@ -32,4 +37,48 @@ function getExpressionPublicWidgetOptions(
};
}

export function getSaveWarningsForExpressionWidget(
widget: ExpressionWidget,
): Array<string> {
const issues: Array<any | string> = [];
const {answerForms, functions} = widget.options;

if (answerForms.length === 0) {
issues.push("No answers specified");
} else {
const hasCorrect = answerForms.some((form) => {
return form.considered === "correct";
});
if (!hasCorrect) {
issues.push("No correct answer specified");
}

answerForms.forEach((form, ix) => {
if (form.value === "") {
issues.push(`Answer ${ix + 1} is empty`);
} else {
// note we're not using icu for content creators
const expression = KAS.parse(form.value, {
functions: functions,
});
if (!expression.parsed) {
issues.push(`Couldn't parse ${form.value}`);
} else if (form.simplify && !expression.expr.isSimplified()) {
issues.push(
`${form.value} isn't simplified, but is required to be`,
);
}
}
});

// The following TODO is transferred over from the expression editor:
// TODO(joel) - warn about:
// - unreachable answers (how??)
// - specific answers following unspecific answers
// - incorrect answers as the final form
}

return issues;
}

export default getExpressionPublicWidgetOptions;
5 changes: 4 additions & 1 deletion packages/perseus-core/src/widgets/expression/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import getExpressionPublicWidgetOptions from "./expression-util";
import getExpressionPublicWidgetOptions, {
getSaveWarningsForExpressionWidget,
} from "./expression-util";

import type {PerseusExpressionWidgetOptions} from "../../data-schema";
import type {WidgetLogic} from "../logic-export.types";
Expand All @@ -23,6 +25,7 @@ const expressionWidgetLogic: WidgetLogic = {
defaultWidgetOptions: defaultWidgetOptions,
defaultAlignment: "inline-block",
getPublicWidgetOptions: getExpressionPublicWidgetOptions,
getSaveWarnings: getSaveWarningsForExpressionWidget,
accessible: true,
};

Expand Down
5 changes: 4 additions & 1 deletion packages/perseus-core/src/widgets/logic-export.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {type getSaveWarningsForRadioWidget} from "./radio/radio-util";
import type getCategorizerPublicWidgetOptions from "./categorizer/categorizer-util";
import type getCSProgramPublicWidgetOptions from "./cs-program/cs-program-util";
import type getDropdownPublicWidgetOptions from "./dropdown/dropdown-util";
import type {getSaveWarningsForExpressionWidget} from "./expression/expression-util";
import type getExpressionPublicWidgetOptions from "./expression/expression-util";
import type getFreeResponsePublicWidgetOptions from "./free-response/free-response-util";
import type getGrapherPublicWidgetOptions from "./grapher/grapher-util";
Expand Down Expand Up @@ -56,7 +57,9 @@ export type PublicWidgetOptionsFunction =
| typeof getSorterPublicWidgetOptions
| typeof getTablePublicWidgetOptions;

export type SaveWarningsFunction = typeof getSaveWarningsForRadioWidget;
export type SaveWarningsFunction =
| typeof getSaveWarningsForRadioWidget
| typeof getSaveWarningsForExpressionWidget;

export type WidgetLogic = {
name: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/perseus-editor/src/widgets/expression-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class ExpressionEditor extends React.Component<Props, State> {
};
}

// TODO(LEMS-3643): Remove this function once the save warnings util is
// complete and in use.
getSaveWarnings: () => any = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am having a hard time figuring out replace the code here with the save warnings code migrated to the expression utils to reduce duplication.

const issues: Array<any | string> = [];

Expand Down
Loading