Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9950fdf
[video-generator] docs(changeset): [Video] Create video widget genera…
nishasy Dec 3, 2025
d9f264b
[free-response-generator] update testdata to use generator
nishasy Dec 3, 2025
5ec5b9c
[free-response-generator] Use widget logic defaults
nishasy Dec 3, 2025
b49d680
[free-response-generator] make sure snapshots didn't change
nishasy Dec 3, 2025
f74620d
[expression-generator] write expression generator
nishasy Dec 4, 2025
60b7438
[expression-generator] replace existing testdata with generator
nishasy Dec 4, 2025
24290be
[expression-generator] More replacements
nishasy Dec 4, 2025
2030852
[expression-generator] update the last of the testdata
nishasy Dec 4, 2025
5978736
[expression-generator] docs(changeset): [Expression] Creat expression…
nishasy Dec 4, 2025
1f1bd5b
[expression-generator] Remove duplicate changeset
nishasy Dec 4, 2025
46e720c
[expression-generator] missed a renderer
nishasy Dec 5, 2025
8a44200
[numeric-input-generator] Write generator
nishasy Dec 5, 2025
5db1304
[numeric-input-generator] replace testdata
nishasy Dec 5, 2025
38e8db9
[numeric-input-generator] replace builder
nishasy Dec 5, 2025
80fd788
[numeric-input-generator] docs(changeset): [Numeric Input] Create num…
nishasy Dec 5, 2025
c282f07
[expression-generator] Oops, some numeric input changes got in here b…
nishasy Dec 5, 2025
8b68e9b
[numeric-input-generator] Merge branch 'expression-generator' into nu…
nishasy Dec 5, 2025
364f051
[expression-generator] Merge branch 'main' into expression-generator
nishasy Dec 8, 2025
47734b1
[numeric-input-generator] Merge branch 'expression-generator' into nu…
nishasy Dec 8, 2025
faa52f9
[numeric-input-generator] fix content
nishasy Dec 8, 2025
367b0db
[expression-generator] bring back generator in extract test
nishasy Dec 8, 2025
30ede82
[expression-generator] Merge branch 'main' into expression-generator
nishasy Dec 8, 2025
ba47415
[numeric-input-generator] Merge branch 'expression-generator' into nu…
nishasy Dec 8, 2025
982074c
[numeric-input-generator] update extraction test after merge
nishasy Dec 8, 2025
511ac4d
[expression-generator] Merge branch 'main' into expression-generator
nishasy Dec 8, 2025
de724d5
[numeric-input-generator] Merge branch 'expression-generator' into nu…
nishasy Dec 8, 2025
df88f31
[numeric-input-generator] Merge branch 'main' into numeric-input-gene…
nishasy Dec 8, 2025
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
8 changes: 8 additions & 0 deletions .changeset/wild-candles-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@khanacademy/perseus": patch
"@khanacademy/perseus-core": patch
"@khanacademy/perseus-editor": patch
"@khanacademy/perseus-score": patch
---

[Numeric Input] Create numeric input widget generator for testdata
5 changes: 5 additions & 0 deletions packages/perseus-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ export {
generateImageOptions,
generateImageWidget,
} from "./utils/generators/image-widget-generator";
export {
generateNumericInputOptions,
generateNumericInputAnswer,
generateNumericInputWidget,
} from "./utils/generators/numeric-input-widget-generator";
export {generateVideoWidget} from "./utils/generators/video-widget-generator";

export {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import {
generateNumericInputAnswer,
generateNumericInputOptions,
generateNumericInputWidget,
} from "./numeric-input-widget-generator";

import type {
NumericInputWidget,
PerseusNumericInputWidgetOptions,
} from "../../data-schema";

describe("generateNumericInputOptions", () => {
it("builds a default numeric input options", () => {
// Arrange, Act
const options: PerseusNumericInputWidgetOptions =
generateNumericInputOptions();

// Assert
expect(options.answers).toEqual([
{
value: null,
status: "correct",
message: "",
simplify: "required",
answerForms: [],
strict: false,
maxError: null,
},
]);
expect(options.size).toBe("normal");
expect(options.coefficient).toBe(false);
expect(options.labelText).toBe("");
expect(options.rightAlign).toBe(false);
expect(options.static).toBe(false);
});

it("builds a numeric input options with all props", () => {
// Arrange, Act
const options: PerseusNumericInputWidgetOptions =
generateNumericInputOptions({
answers: [
{
value: 42,
status: "wrong",
message: "test-answer",
simplify: "optional",
answerForms: ["integer", "decimal"],
strict: true,
maxError: 0.1,
},
],
size: "small",
coefficient: true,
labelText: "Enter your answer",
rightAlign: true,
static: true,
});

// Assert
expect(options.answers).toEqual([
{
value: 42,
status: "wrong",
message: "test-answer",
simplify: "optional",
answerForms: ["integer", "decimal"],
strict: true,
maxError: 0.1,
},
]);
expect(options.size).toBe("small");
expect(options.coefficient).toBe(true);
expect(options.labelText).toBe("Enter your answer");
expect(options.rightAlign).toBe(true);
expect(options.static).toBe(true);
});

it("builds a numeric input options with answer generator default", () => {
// Arrange, Act
const options: PerseusNumericInputWidgetOptions =
generateNumericInputOptions({
answers: [generateNumericInputAnswer()],
size: "normal",
coefficient: false,
labelText: "",
rightAlign: false,
});

// Assert
expect(options.answers).toEqual([
{
value: null,
status: "correct",
message: "",
simplify: "required",
answerForms: [],
strict: false,
maxError: null,
},
]);
expect(options.size).toBe("normal");
expect(options.coefficient).toBe(false);
expect(options.labelText).toBe("");
expect(options.rightAlign).toBe(false);
});

it("builds a numeric input options with answer generator specified", () => {
// Arrange, Act
const options: PerseusNumericInputWidgetOptions =
generateNumericInputOptions({
answers: [
generateNumericInputAnswer({
value: 100,
status: "correct",
message: "Great job!",
simplify: "enforced",
answerForms: ["improper", "mixed"],
strict: true,
maxError: 0.5,
}),
],
size: "small",
coefficient: true,
});

// Assert
expect(options.answers).toEqual([
{
value: 100,
status: "correct",
message: "Great job!",
simplify: "enforced",
answerForms: ["improper", "mixed"],
strict: true,
maxError: 0.5,
},
]);
expect(options.size).toBe("small");
expect(options.coefficient).toBe(true);
});

it("builds a numeric input options with multiple answers", () => {
// Arrange, Act
const options: PerseusNumericInputWidgetOptions =
generateNumericInputOptions({
answers: [
generateNumericInputAnswer({
value: 42,
status: "correct",
}),
generateNumericInputAnswer({
value: 43,
status: "wrong",
message: "Close, but not quite!",
}),
],
});

// Assert
expect(options.answers).toHaveLength(2);
expect(options.answers[0].value).toBe(42);
expect(options.answers[0].status).toBe("correct");
expect(options.answers[1].value).toBe(43);
expect(options.answers[1].status).toBe("wrong");
expect(options.answers[1].message).toBe("Close, but not quite!");
});
});

describe("generateNumericInputWidget", () => {
it("builds a default numeric input widget", () => {
// Arrange, Act
const widget: NumericInputWidget = generateNumericInputWidget();

// Assert
expect(widget.type).toBe("numeric-input");
expect(widget.graded).toBe(true);
expect(widget.static).toBe(false);
expect(widget.version).toEqual({major: 0, minor: 0});
expect(widget.alignment).toBe("default");
expect(widget.options).toEqual({
answers: [
{
value: null,
status: "correct",
message: "",
simplify: "required",
answerForms: [],
strict: false,
maxError: null,
},
],
size: "normal",
coefficient: false,
labelText: "",
rightAlign: false,
static: false,
});
});

it("builds a numeric input widget with all props", () => {
// Arrange, Act
const widget: NumericInputWidget = generateNumericInputWidget({
graded: false,
version: {major: 1, minor: 2},
static: true,
alignment: "block",
options: {
answers: [
{
value: 42,
status: "wrong",
message: "test-answer",
simplify: "optional",
answerForms: ["integer", "decimal"],
strict: true,
maxError: 0.1,
},
],
size: "small",
coefficient: true,
labelText: "Enter your answer",
rightAlign: true,
static: true,
},
});

// Assert
expect(widget.static).toBe(true);
expect(widget.graded).toBe(false);
expect(widget.version).toEqual({major: 1, minor: 2});
expect(widget.alignment).toBe("block");
expect(widget.options).toEqual({
answers: [
{
value: 42,
status: "wrong",
message: "test-answer",
simplify: "optional",
answerForms: ["integer", "decimal"],
strict: true,
maxError: 0.1,
},
],
size: "small",
coefficient: true,
labelText: "Enter your answer",
rightAlign: true,
static: true,
});
});

it("adds options when option generator is used", () => {
// Arrange, Act
const widget: NumericInputWidget = generateNumericInputWidget({
static: true,
alignment: "block",
// Use the option generator to build the options
options: generateNumericInputOptions({
answers: [
generateNumericInputAnswer({
value: 123,
message: "Perfect!",
}),
],
size: "small",
coefficient: true,
labelText: "Type your answer",
}),
});

// Assert
expect(widget.static).toBe(true);
expect(widget.alignment).toBe("block");
expect(widget.options.answers).toEqual([
{
value: 123,
status: "correct",
message: "Perfect!",
simplify: "required",
answerForms: [],
strict: false,
maxError: null,
},
]);
expect(widget.options.size).toBe("small");
expect(widget.options.coefficient).toBe(true);
expect(widget.options.labelText).toBe("Type your answer");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import numericInputWidgetLogic from "../../widgets/numeric-input";

import type {
PerseusNumericInputWidgetOptions,
NumericInputWidget,
PerseusNumericInputAnswer,
} from "../../data-schema";

export function generateNumericInputOptions(
options?: Partial<PerseusNumericInputWidgetOptions>,
): PerseusNumericInputWidgetOptions {
return {
...numericInputWidgetLogic.defaultWidgetOptions,
static: false,
...options,
};
}

export function generateNumericInputAnswer(
answerOptions?: Partial<PerseusNumericInputAnswer>,
): PerseusNumericInputAnswer {
return {
...numericInputWidgetLogic.defaultWidgetOptions.answers[0],
...answerOptions,
};
}

export function generateNumericInputWidget(
numericInputWidgetProperties?: Partial<Omit<NumericInputWidget, "type">>,
): NumericInputWidget {
return {
type: "numeric-input",
graded: true,
version: {major: 0, minor: 0},
static: false,
alignment: "default",
options: generateNumericInputOptions(),
...numericInputWidgetProperties,
};
}
Loading
Loading