Skip to content

Commit

Permalink
Create randomized storybook stories for Radio, Categorizer, Dropdown,…
Browse files Browse the repository at this point in the history
… Explanation, Expression, and interactive graph (#788)

## Summary:
Created during hackathon 2023 - Basically manual fuzz testing - random
configurations for widgets.

Issue: None

## Test plan:
None
  • Loading branch information
jeanettehead authored Nov 28, 2023
2 parents 7972654 + 9335d95 commit 427ea07
Show file tree
Hide file tree
Showing 11 changed files with 698 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/hot-apes-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

create randomized storybook stories for some widgets
11 changes: 8 additions & 3 deletions packages/perseus/src/perseus-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ export type PerseusInteractiveGraphWidgetOptions = {
showTooltips?: boolean;
// The unit to show on the ruler. e.g. "mm", "cm", "m", "km", "in", "ft", "yd", "mi"
rulerLabel: string;
// How many ticks to show on the ruler. e.g. 1, 2, 4, 8, 10, 16
// How many ticks to show on the ruler. e.g. 1, 2, 4, 8, 10, 16. Must be an integer.
rulerTicks: number;
// The X and Y coordinate ranges for the view of the graph. default: [[-10, 10], [-10, 10]]
// NOTE(kevinb): perseus_data.go defines this as Array<Array<number>>
Expand Down Expand Up @@ -577,6 +577,7 @@ export type PerseusGraphTypeAngle = {
snapDegrees?: number;
// How to match the answer. If missing, defaults to exact matching.
match?: "congruent";
// must have 3 coords - ie [Coord, Coord, Coord]
coords?: ReadonlyArray<Coord>;
};

Expand All @@ -588,11 +589,13 @@ export type PerseusGraphTypeCircle = {

export type PerseusGraphTypeLinear = {
type: "linear";
// expects 2 coords
coords?: ReadonlyArray<Coord>;
} & PerseusGraphTypeCommon;

export type PerseusGraphTypeLinearSystem = {
type: "linear-system";
// expects 2 sets of 2 coords
coords?: ReadonlyArray<ReadonlyArray<Coord>>;
} & PerseusGraphTypeCommon;

Expand Down Expand Up @@ -620,25 +623,27 @@ export type PerseusGraphTypePolygon = {

export type PerseusGraphTypeQuadratic = {
type: "quadratic";
// expects a list of 3 coords
coords?: ReadonlyArray<Coord>;
} & PerseusGraphTypeCommon;

export type PerseusGraphTypeSegment = {
type: "segment";
// The number of segments if a "segment" type. default: 1. Max: 6
numSegments?: number;
// A list of segments (each segment is a list of coordinates).
// Length should match the `numSegments` value.
// Expects a list of Coord tuples. Length should match the `numSegments` value.
coords?: ReadonlyArray<ReadonlyArray<Coord>>;
} & PerseusGraphTypeCommon;

export type PerseusGraphTypeSinusoid = {
type: "sinusoid";
// Expects a list of 2 Coords
coords?: ReadonlyArray<Coord>;
} & PerseusGraphTypeCommon;

export type PerseusGraphTypeRay = {
type: "ray";
// Expects a list of 2 Coords
coords?: ReadonlyArray<Coord>;
} & PerseusGraphTypeCommon;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Button from "@khanacademy/wonder-blocks-button";
import * as React from "react";

import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";
import {randomCategorizerGenerator} from "../__testdata__/categorizer.testdata";
import {randomDropdownGenerator} from "../__testdata__/dropdown.testdata";
import {randomExplanationGenerator} from "../__testdata__/explanation.testdata";
import {randomExpressionGenerator} from "../__testdata__/expression.testdata";
import {randomInteractiveGraphGenerator} from "../__testdata__/interactive-graph-random.testdata";
import {randomRadioGenerator} from "../__testdata__/radio.testdata";

import type {PerseusRenderer} from "@khanacademy/perseus";

export default {
title: "Perseus/Randomized Widgets",
};

const RandomizedWidgetContainer = (
randomizer: () => PerseusRenderer,
): React.ReactElement => {
const [question, setQuestion] = React.useState(randomizer());
const randomize = () => {
setQuestion(randomizer());
};

return (
<div>
<Button size="small" style={{marginBottom: 24}} onClick={randomize}>
Randomize
</Button>

<RendererWithDebugUI question={question} />
</div>
);
};

export const RandomCategorizer = (): React.ReactElement => {
return RandomizedWidgetContainer(randomCategorizerGenerator);
};

export const RandomDropdown = (): React.ReactElement => {
return RandomizedWidgetContainer(randomDropdownGenerator);
};

export const RandomExplanation = (): React.ReactElement => {
return RandomizedWidgetContainer(randomExplanationGenerator);
};

export const RandomExpression = (): React.ReactElement => {
return RandomizedWidgetContainer(randomExpressionGenerator);
};

export const RandomInteractiveGraph = (): React.ReactElement => {
return RandomizedWidgetContainer(randomInteractiveGraphGenerator);
};

export const RandomRadio = (): React.ReactElement => {
return RandomizedWidgetContainer(randomRadioGenerator);
};
40 changes: 40 additions & 0 deletions packages/perseus/src/widgets/__testdata__/categorizer.testdata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
arrayOfLength,
randomBoolean,
randomInteger,
randomSentence,
} from "./randomizers";

import type {PerseusRenderer} from "../../perseus-types";

export const question1: PerseusRenderer = {
Expand Down Expand Up @@ -47,3 +54,36 @@ export const question1: PerseusRenderer = {
},
},
};

export const randomCategorizerGenerator = (): PerseusRenderer => {
const questionText = randomSentence(35);
const numItems = randomInteger(2, 7);
const numCategories = randomInteger(2, 6);

return {
content: `${questionText}\n\n\n[[\u2603 categorizer 1]]`,
images: {},
widgets: {
"categorizer 1": {
version: {major: 0, minor: 0},
type: "categorizer",
graded: randomBoolean(),
alignment: "default",
options: {
items: arrayOfLength(numItems).map(() =>
randomSentence(12),
),
values: arrayOfLength(numItems).map(() =>
randomInteger(0, numCategories - 1),
),
randomizeItems: randomBoolean(),
categories: arrayOfLength(numCategories).map(() =>
randomSentence(7),
),
highlightLint: randomBoolean(),
static: randomBoolean(0.05),
},
},
},
};
};
38 changes: 38 additions & 0 deletions packages/perseus/src/widgets/__testdata__/dropdown.testdata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
arrayOfLength,
randomBoolean,
randomInteger,
randomSentence,
} from "./randomizers";

import type {PerseusRenderer} from "../../perseus-types";

export const question1: PerseusRenderer = {
Expand Down Expand Up @@ -31,3 +38,34 @@ export const question1: PerseusRenderer = {
},
},
};

export const randomDropdownGenerator = (): PerseusRenderer => {
const numChoices = randomInteger(2, 16);
const correctIndex = randomInteger(0, numChoices - 1);
return {
content: `${randomSentence(20)} [[☃ dropdown 1]] ${randomSentence(10)}`,
images: {},
widgets: {
"dropdown 1": {
type: "dropdown",
alignment: "default",
static: randomBoolean(0.05),
graded: randomBoolean(),
options: {
static: randomBoolean(0.05),
placeholder: randomSentence(10),
choices: arrayOfLength(numChoices).map((_, i) => {
return {
content: randomSentence(10),
correct: i === correctIndex,
};
}),
},
version: {
major: 0,
minor: 0,
},
},
},
};
};
30 changes: 30 additions & 0 deletions packages/perseus/src/widgets/__testdata__/explanation.testdata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {randomBoolean, randomSentence} from "./randomizers";

import type {PerseusRenderer} from "../../perseus-types";

export const question1: PerseusRenderer = {
Expand Down Expand Up @@ -53,3 +55,31 @@ export const question2: PerseusRenderer = {
},
},
};

export const randomExplanationGenerator = (): PerseusRenderer => {
return {
content: `${randomSentence(
50,
)}\n[[\u2603 explanation 1]]\n${randomSentence(50)}`,
images: {},
widgets: {
"explanation 1": {
graded: randomBoolean(),
version: {
major: 0,
minor: 0,
},
static: randomBoolean(0.05),
type: "explanation",
options: {
hidePrompt: `${randomSentence(20)}`,
widgets: {},
explanation: `${randomSentence(50)}`,
static: randomBoolean(0.05),
showPrompt: `${randomSentence(7)}`,
},
alignment: "default",
},
},
};
};
82 changes: 82 additions & 0 deletions packages/perseus/src/widgets/__testdata__/expression.testdata.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import {
arrayOfLength,
randomBoolean,
randomElement,
randomInteger,
randomLetter,
randomSentence,
randomWord,
} from "./randomizers";

import type {
PerseusExpressionWidgetOptions,
Version,
PerseusItem,
PerseusRenderer,
ExpressionWidget,
} from "../../perseus-types";

const createItemJson = (
Expand Down Expand Up @@ -96,3 +108,73 @@ export const expressionItem3: PerseusItem = createItemJson(
minor: 0,
},
);

export const randomExpressionGenerator = (): PerseusRenderer => {
const randomButtonSet = [
"basic",
"basic+div",
"trig",
"prealgebra",
"logarithms",
"basic relations",
"advanced relations",
]
.sort(() => {
return randomBoolean() ? 1 : -1;
})
.slice(0, randomInteger(1, 6));

const randomFunctionSet = arrayOfLength(randomInteger(0, 6)).map(
randomLetter,
);

return {
content: `${randomSentence(20)} [[☃ expression 1]]`,
images: {},
widgets: {
"expression 1": {
type: "expression",
graded: randomBoolean(),
version: {
major: 1,
minor: 0,
},
static: randomBoolean(0.05),
options: {
answerForms: [
{
considered: randomElement([
"correct",
"wrong",
"ungraded",
]),
form: randomBoolean(),
simplify: randomBoolean(),
value: randomWord(),
},
{
considered: randomElement([
"correct",
"wrong",
"ungraded",
]),
form: randomBoolean(),
simplify: randomBoolean(),
value: randomWord(),
},
],
times: randomBoolean(),
buttonSets: randomButtonSet,
functions: randomFunctionSet,
buttonsVisible: randomElement([
"always",
"never",
"focused",
undefined,
]),
alignment: "default",
},
} as ExpressionWidget,
},
};
};
Loading

0 comments on commit 427ea07

Please sign in to comment.