@khanacademy/perseus 11.5.1 → 11.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,7 +36,7 @@ export declare class NumericInput extends React.Component<Props, State> {
36
36
  static defaultProps: DefaultProps;
37
37
  static getUserInputFromProps(props: Props): UserInput;
38
38
  static getOneCorrectAnswerFromRubric(rubric: Rubric): string | null | undefined;
39
- static validate(useInput: UserInput, rubric: Rubric): PerseusScore;
39
+ static validate(userInput: UserInput, rubric: Rubric): PerseusScore;
40
40
  state: State;
41
41
  examples: () => ReadonlyArray<string>;
42
42
  shouldShowExamples: () => boolean;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core Perseus API (includes renderers and widgets)",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "11.5.1",
6
+ "version": "11.6.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.1",
26
26
  "@khanacademy/kmath": "^0.1.2",
27
- "@khanacademy/math-input": "^13.1.0",
27
+ "@khanacademy/math-input": "^14.0.0",
28
28
  "@khanacademy/perseus-core": "1.1.0",
29
29
  "@khanacademy/perseus-linter": "^0.3.4",
30
30
  "@khanacademy/pure-markdown": "^0.2.6",
@@ -189,7 +189,7 @@ exports[`server item renderer should snapshot: initial render 1`] = `
189
189
  class=""
190
190
  >
191
191
  <div
192
- class="perseus-hint-renderer"
192
+ class="hint perseus-hint-renderer"
193
193
  tabindex="-1"
194
194
  >
195
195
  <span
@@ -219,7 +219,7 @@ exports[`server item renderer should snapshot: initial render 1`] = `
219
219
  </div>
220
220
  </div>
221
221
  <div
222
- class="perseus-hint-renderer"
222
+ class="hint perseus-hint-renderer"
223
223
  tabindex="-1"
224
224
  >
225
225
  <span
@@ -249,7 +249,7 @@ exports[`server item renderer should snapshot: initial render 1`] = `
249
249
  </div>
250
250
  </div>
251
251
  <div
252
- class="perseus-hint-renderer last-hint last-rendered"
252
+ class="hint perseus-hint-renderer last-hint last-rendered"
253
253
  tabindex="-1"
254
254
  >
255
255
  <span
@@ -1,5 +1,6 @@
1
+ import {StatefulKeypadContextProvider} from "@khanacademy/math-input";
1
2
  import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
2
- import {fireEvent, render, screen} from "@testing-library/react";
3
+ import {fireEvent, render, screen, waitFor} from "@testing-library/react";
3
4
  import userEvent from "@testing-library/user-event";
4
5
  import * as React from "react";
5
6
  import "@testing-library/jest-dom"; // Imports custom matchers
@@ -71,23 +72,25 @@ export const renderQuestion = (
71
72
  let renderer: ItemRenderer | null = null;
72
73
  const {container} = render(
73
74
  <RenderStateRoot>
74
- <ItemRenderer
75
- ref={(node) => (renderer = node)}
76
- apiOptions={apiOptions}
77
- item={question}
78
- problemNum={0}
79
- reviewMode={false}
80
- savedState=""
81
- controlPeripherals={false}
82
- dependencies={testDependenciesV2}
83
- {...optionalProps}
84
- />
85
- {/* The ItemRenderer _requires_ two divs: a work area and hints
75
+ <StatefulKeypadContextProvider>
76
+ <ItemRenderer
77
+ ref={(node) => (renderer = node)}
78
+ apiOptions={apiOptions}
79
+ item={question}
80
+ problemNum={0}
81
+ reviewMode={false}
82
+ savedState=""
83
+ controlPeripherals={false}
84
+ dependencies={testDependenciesV2}
85
+ {...optionalProps}
86
+ />
87
+ {/* The ItemRenderer _requires_ two divs: a work area and hints
86
88
  area. Without both of these, it fails to render anything! */}
87
- <div id="workarea" />
88
- <div id="hintsarea" />
89
+ <div id="workarea" />
90
+ <div id="hintsarea" />
89
91
 
90
- <Peripherals />
92
+ <Peripherals />
93
+ </StatefulKeypadContextProvider>
91
94
  </RenderStateRoot>,
92
95
  );
93
96
  if (!renderer) {
@@ -309,18 +312,21 @@ describe("item renderer", () => {
309
312
  );
310
313
  });
311
314
 
312
- it("should activate the keypad when widget with input is focused", () => {
315
+ it("should activate the keypad when widget with input is focused", async () => {
313
316
  // Arrange
314
317
  const {renderer} = renderQuestion(itemWithInput, {
315
318
  isMobile: true,
316
319
  customKeypad: true,
320
+ useV2Keypad: true,
317
321
  });
318
322
 
319
323
  // Act
320
324
  renderer.focus();
321
325
 
322
326
  // Assert
323
- expect(screen.getByLabelText("7")).toBeVisible();
327
+ await waitFor(() => {
328
+ expect(screen.getByLabelText("7")).toBeVisible();
329
+ });
324
330
  });
325
331
 
326
332
  it("should provide current and previous focus paths on focus change to, and away from, a single widget", () => {
@@ -62,6 +62,7 @@ class HintRenderer extends React.Component<Props> {
62
62
  const {isMobile} = apiOptions;
63
63
 
64
64
  const classNames = classnames(
65
+ "hint",
65
66
  !isMobile && "perseus-hint-renderer",
66
67
  isMobile && css(styles.newHint),
67
68
  isMobile && lastRendered && css(styles.lastRenderedNewHint),
@@ -1128,7 +1128,7 @@ exports[`multi-item renderer should snapshot: initial render 1`] = `
1128
1128
  class=""
1129
1129
  >
1130
1130
  <div
1131
- class="perseus-hint-renderer last-rendered"
1131
+ class="hint perseus-hint-renderer last-rendered"
1132
1132
  tabindex="-1"
1133
1133
  >
1134
1134
  <span
@@ -762,7 +762,8 @@ export type PerseusNumericInputAnswer = {
762
762
  // The forms available for this answer. Options: "integer, ""decimal", "proper", "improper", "mixed", or "pi"
763
763
  // NOTE: perseus_data.go says this is required even though it isn't necessary.
764
764
  answerForms?: ReadonlyArray<MathFormat>;
765
- // Whether the answerForms should be strictly matched
765
+ // Whether we should check the answer strictly against the the configured answerForms (strict = true)
766
+ // or include the set of default answerForms (strict = false).
766
767
  strict: boolean;
767
768
  // A range of error +/- the value
768
769
  // NOTE: perseus_data.go says this is non-nullable even though we handle null values.
@@ -10,6 +10,12 @@ var {
10
10
  font-style: normal;
11
11
  }
12
12
 
13
+ .hint.last-hint {
14
+ .paragraph {
15
+ font-weight: bold;
16
+ }
17
+ }
18
+
13
19
  .hint_blue {
14
20
  color: #6495ed;
15
21
  }
@@ -32,10 +38,6 @@ var {
32
38
  color: #9d38bd;
33
39
  }
34
40
 
35
- .last-hint {
36
- font-weight: bold;
37
- }
38
-
39
41
  div.subhint {
40
42
  border: 1px solid #aaaaaa;
41
43
  background: #f9f9f9;
@@ -144,7 +144,6 @@
144
144
  }
145
145
 
146
146
  .paragraph ul:not(.perseus-widget-radio) {
147
- .legacy-typography;
148
147
  padding-left: 35px;
149
148
  list-style-type: disc;
150
149
  }
@@ -1,4 +1,8 @@
1
- import {KeypadContext, MobileKeypad} from "@khanacademy/math-input";
1
+ import {
2
+ KeypadContext,
3
+ StatefulKeypadContextProvider,
4
+ MobileKeypad,
5
+ } from "@khanacademy/math-input";
2
6
  import {View} from "@khanacademy/wonder-blocks-core";
3
7
  import {action} from "@storybook/addon-actions";
4
8
  import {StyleSheet} from "aphrodite";
@@ -29,29 +33,11 @@ type Props = {
29
33
  };
30
34
 
31
35
  const TestKeypadContextWrapper = (props: Props): React.ReactElement => {
32
- const [keypadElement, setKeypadElement] = React.useState(null);
33
- const [renderer, setRenderer] = React.useState(null);
34
- const [scrollableElement, setScrollableElement] = React.useState(
35
- document.body,
36
- );
37
-
38
36
  return (
39
- <KeypadContext.Provider
40
- value={{
41
- // @ts-expect-error - TS2322 - Type 'Dispatch<SetStateAction<HTMLElement>>' is not assignable to type '(scrollableElement?: HTMLElement | null | undefined) => void'.
42
- setKeypadElement,
43
- keypadElement,
44
- // @ts-expect-error - TS2322 - Type 'Dispatch<SetStateAction<HTMLElement>>' is not assignable to type '(scrollableElement?: HTMLElement | null | undefined) => void'.
45
- setRenderer,
46
- renderer,
47
- // @ts-expect-error - TS2322 - Type 'Dispatch<SetStateAction<HTMLElement>>' is not assignable to type '(scrollableElement?: HTMLElement | null | undefined) => void'.
48
- setScrollableElement,
49
- scrollableElement,
50
- }}
51
- >
37
+ <StatefulKeypadContextProvider>
52
38
  {props.children}
53
39
  <Footer />
54
- </KeypadContext.Provider>
40
+ </StatefulKeypadContextProvider>
55
41
  );
56
42
  };
57
43
 
@@ -1,5 +1,9 @@
1
1
  import "@testing-library/jest-dom";
2
- import {KeypadContext, MobileKeypad} from "@khanacademy/math-input";
2
+ import {
3
+ KeypadContext,
4
+ StatefulKeypadContextProvider,
5
+ MobileKeypad,
6
+ } from "@khanacademy/math-input";
3
7
  import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
4
8
  import {
5
9
  fireEvent,
@@ -22,8 +26,6 @@ import {registerWidget} from "../../widgets";
22
26
  import {expressionItem2} from "../__testdata__/expression.testdata";
23
27
  import ExpressionExport from "../expression";
24
28
 
25
- import type {KeypadAPI} from "@khanacademy/math-input";
26
-
27
29
  const MQ = MathQuill.getInterface(2);
28
30
 
29
31
  function RendererWithContext({item}) {
@@ -67,27 +69,12 @@ function KeypadWithContext() {
67
69
  }
68
70
 
69
71
  function ConnectedRenderer({item = expressionItem2}) {
70
- const [keypadElement, setKeypadElement] =
71
- React.useState<KeypadAPI | null>();
72
- const [renderer, setRenderer] = React.useState<any>(null);
73
- const [scrollableElement, setScrollableElement] =
74
- React.useState<HTMLElement | null>();
75
-
76
72
  return (
77
73
  <RenderStateRoot>
78
- <KeypadContext.Provider
79
- value={{
80
- setKeypadElement,
81
- keypadElement,
82
- setRenderer,
83
- renderer,
84
- setScrollableElement,
85
- scrollableElement,
86
- }}
87
- >
74
+ <StatefulKeypadContextProvider>
88
75
  <RendererWithContext item={item} />
89
76
  <KeypadWithContext />
90
- </KeypadContext.Provider>
77
+ </StatefulKeypadContextProvider>
91
78
  </RenderStateRoot>
92
79
  );
93
80
  }
@@ -250,6 +250,41 @@ describe("static function validate", () => {
250
250
  expect(score.message?.includes("pi")).toBeFalsy();
251
251
  });
252
252
 
253
+ it("still validates against pi if provided in answerForms", () => {
254
+ const rubric: Rubric = {
255
+ answers: [
256
+ {
257
+ maxError: null,
258
+ message: "",
259
+ simplify: "required",
260
+ status: "correct",
261
+ strict: false,
262
+ value: 311.01767270538954,
263
+ answerForms: ["pi"],
264
+ },
265
+ ],
266
+ labelText: "",
267
+ size: "normal",
268
+ static: false,
269
+ coefficient: false,
270
+ };
271
+
272
+ const userInput = {
273
+ currentValue: "99 pi",
274
+ } as const;
275
+
276
+ const score = NumericInput.validate(userInput, rubric);
277
+
278
+ expect(score).toMatchInlineSnapshot(`
279
+ {
280
+ "earned": 1,
281
+ "message": null,
282
+ "total": 1,
283
+ "type": "points",
284
+ }
285
+ `);
286
+ });
287
+
253
288
  it("with a strict answer", () => {
254
289
  const rubric: Rubric = {
255
290
  answers: [
@@ -15,6 +15,7 @@ import type {
15
15
  PerseusNumericInputAnswer,
16
16
  PerseusNumericInputWidgetOptions,
17
17
  PerseusNumericInputAnswerForm,
18
+ MathFormat,
18
19
  } from "../perseus-types";
19
20
  import type {
20
21
  FocusPath,
@@ -25,7 +26,11 @@ import type {
25
26
 
26
27
  const ParseTex = TexWrangler.parseTex;
27
28
 
28
- const answerFormButtons = [
29
+ const answerFormButtons: ReadonlyArray<{
30
+ title: string;
31
+ value: MathFormat;
32
+ content: string;
33
+ }> = [
29
34
  {title: "Integers", value: "integer", content: "6"},
30
35
  {title: "Decimals", value: "decimal", content: "0.75"},
31
36
  {title: "Proper fractions", value: "proper", content: "\u2157"},
@@ -149,8 +154,8 @@ export class NumericInput extends React.Component<Props, State> {
149
154
  return answerStrings[0];
150
155
  }
151
156
 
152
- static validate(useInput: UserInput, rubric: Rubric): PerseusScore {
153
- const allAnswerForms = answerFormButtons
157
+ static validate(userInput: UserInput, rubric: Rubric): PerseusScore {
158
+ const defaultAnswerForms = answerFormButtons
154
159
  .map((e) => e["value"])
155
160
  // Don't default to validating the answer as a pi answer
156
161
  // if answerForm isn't set on the answer
@@ -159,6 +164,18 @@ export class NumericInput extends React.Component<Props, State> {
159
164
 
160
165
  const createValidator = (answer: PerseusNumericInputAnswer) => {
161
166
  const stringAnswer = `${answer.value}`;
167
+
168
+ // Always validate against the provided answer forms (pi, decimal, etc.)
169
+ const validatorForms = [...(answer.answerForms ?? [])];
170
+
171
+ // When an answer is set to strict, we validate using ONLY
172
+ // the provided answerForms. If strict is false, or if there
173
+ // were no provided answer forms, we will include all
174
+ // of the default answer forms in our validator.
175
+ if (!answer.strict || validatorForms.length === 0) {
176
+ validatorForms.push(...defaultAnswerForms);
177
+ }
178
+
162
179
  return KhanAnswerTypes.number.createValidatorFunctional(
163
180
  stringAnswer,
164
181
  {
@@ -169,19 +186,14 @@ export class NumericInput extends React.Component<Props, State> {
169
186
  : "optional",
170
187
  inexact: true, // TODO(merlob) backfill / delete
171
188
  maxError: answer.maxError,
172
- forms:
173
- answer.strict &&
174
- answer.answerForms &&
175
- answer.answerForms.length !== 0
176
- ? answer.answerForms
177
- : allAnswerForms,
189
+ forms: validatorForms,
178
190
  },
179
191
  );
180
192
  };
181
193
 
182
194
  // We may have received TeX; try to parse it before grading.
183
195
  // If `currentValue` is not TeX, this should be a no-op.
184
- const currentValue = ParseTex(useInput.currentValue);
196
+ const currentValue = ParseTex(userInput.currentValue);
185
197
  const correctAnswers = rubric.answers.filter(
186
198
  (answer) => answer.status === "correct",
187
199
  );