-
Notifications
You must be signed in to change notification settings - Fork 350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactoring Numeric Input helper functions to remove underscore #2128
base: feature/numeric-dx-refactor
Are you sure you want to change the base?
Refactoring Numeric Input helper functions to remove underscore #2128
Conversation
… remove underscore, improve documentation, and add tests. Issue: LEMS-2446
…lper functions to remove underscore, improve documentation, and add tests.
@@ -1210,7 +1212,7 @@ export type PerseusNumericInputAnswer = { | |||
// NOTE: perseus_data.go says this is non-nullable even though we handle null values. | |||
maxError: number | null | undefined; | |||
// Unsimplified answers are Ungraded, Accepted, or Wrong. Options: "required", "correct", or "enforced" | |||
simplify: string | null | undefined; | |||
simplify: PerseusNumericInputSimplify; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a spicy move, but I think it's good to be more specific with our types here. While it resulted in some minor downstream updates, I think greater specificity will help us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merging the | null | undefined
into the type feels like it hides something important. Without referring to the type, this field looks required, but you find out that it's optional only when you inspect the new PerseusNumericInputSimplify
.
I'm not sure if we have a precedent, but I think I'd prefer we kept the new type as the union of string values and then add | null | undefined
here so that the "optionality" stays in situ.
}>; | ||
labelText: string; | ||
size: "normal" | "small"; | ||
answerForms: ReadonlyArray<PerseusNumericInputAnswerForm>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have this type, no need to force ourselves to maintain two locations.
@@ -376,35 +374,3 @@ describe("Numeric input widget", () => { | |||
); | |||
}); | |||
}); | |||
|
|||
describe("unionAnswerForms utility function", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created better tests over in utils.test.ts
for this logic.
npm Snapshot: PublishedGood news!! We've packaged up the latest commit from this PR (ad871e0) and published it to npm. You Example: yarn add @khanacademy/perseus@PR2128 If you are working in Khan Academy's webapp, you can run: ./dev/tools/bump_perseus_version.sh -t PR2128 |
Size Change: -70 B (0%) Total Size: 1.47 MB
ℹ️ View Unchanged
|
@@ -15,7 +16,7 @@ describe("generateExamples", () => { | |||
simplify: "required", | |||
}, | |||
]; | |||
const strings: Partial<PerseusStrings> = { | |||
const fakePerseusStrings: Partial<PerseusStrings> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly a self nit, but it seemed good to be really specific that I'm faking the strings here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: could we not use the mock strings from
perseus/packages/perseus/src/strings.ts
Line 551 in 9cabe68
export const mockStrings: PerseusStrings = { |
…e that it's no longer generic as it was not being used elsewhere.
* answer forms. This is useful for ensuring that we don't show duplicate examples | ||
* to the user. | ||
*/ | ||
const getUniqueAnswerForms = function ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be simpler to implement something like how number line tick marks are handled? Perhaps something like:
const allFormNames = list.map((form) => form.name);
const uniqueFormNames = [...new Set(allFormNames)];
return uniqueFormNames.map((name) => list.find((form) => form.name === name));
Not sure if simplify is needed in this evaluation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modded my approach slightly but you're right, this could be simplified even more! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This did make me think deeper about which examples we're filtering: the way we've historically generated these examples seems antithetical to how we intend Content Creators to create answers.
Say I want to make a course focused on fractions, and I don't want kids simply entering decimals. If I make a wrong answer that's strictly set to match a decimal, and a right answer set to strictly match a fraction, the provided tooltip will read: "Your answer should be - a simplified proper fraction - an exact decimal like 0.75". This would directly mislead the user as their answer specifically shouldn't be a decimal.🤔
This is how the logic works currently so it's not a regression. It does make me wonder if we should only be providing the correct answers to the tooltip however. It seems like an easy improvement that we could make. Perhaps a ticket for emergent work?
Edit: Actually, it might be nice to have a section for "Your answer should not be".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Perhaps we can get confirmation from Content folks that only correct answers should be included in the tooltip? Though, I wonder if the scoring logic would fail when a "wrong" answer matches a "correct" answer (i.e. the decimal equivalent of a fraction).
// If the Content Creator has not specified any answer forms, | ||
// we do not need to show any examples. | ||
if (answerForms.length === 0) { | ||
return []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to call out that I've technically changed this functionality.
Previously we would return a string with all of the example strings that we just never displayed. This seemed like unnecessary logic so I've updated it to simply exit early in such a case.
…d no longer generating example strings if we aren't going to show them.
…s we don't need the full scope of what isEqual was providing previously. <see below>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry... drive by review comments. 🫶
@@ -1210,7 +1212,7 @@ export type PerseusNumericInputAnswer = { | |||
// NOTE: perseus_data.go says this is non-nullable even though we handle null values. | |||
maxError: number | null | undefined; | |||
// Unsimplified answers are Ungraded, Accepted, or Wrong. Options: "required", "correct", or "enforced" | |||
simplify: string | null | undefined; | |||
simplify: PerseusNumericInputSimplify; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merging the | null | undefined
into the type feels like it hides something important. Without referring to the type, this field looks required, but you find out that it's optional only when you inspect the new PerseusNumericInputSimplify
.
I'm not sure if we have a precedent, but I think I'd prefer we kept the new type as the union of string values and then add | null | undefined
here so that the "optionality" stays in situ.
@@ -93,7 +93,7 @@ class InputWithExamples extends React.Component<Props, State> { | |||
const ariaId = `aria-for-${id}`; | |||
// Generate text from a known set of format options that will read well in a screen reader | |||
const examplesAria = | |||
this.props.examples.length === 7 | |||
this.props.examples.length === 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original 7
feels very magic. Why are we now changing it to 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously we were looping through to return all of the example strings when the teacher had not selected any answerForms
. Then we would never show these strings as shouldShowExamples
would hide them in this case.
There are 6 example strings, and then we prepend a "Your answer should be" as the first string (for a total of 7). In the case where teachers have selected all 6 answerForms, we still don't show these answerForms as they are effectively useless.
const forms =
this.props.answerForms?.length !== 0
? this.props.answerForms
: Object.keys(formExamples).map((name) => {
return {
name: name,
simplify: "required",
} as PerseusNumericInputAnswerForm;
});
That seemed a little silly to me, so I've updated the function to exit early in such a case, returning an empty array. That meant that I could update this section to make more sense. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohhhh... Nice. Thanks for cleaning that up!
const rendererProps: RenderProps & {answers?: any} = { | ||
...widgetOptions, | ||
answerForms: unionAnswerForms( | ||
// Pull out the name of each form and whether that form has | ||
// required simplification. | ||
widgetOptions.answers.map((answer) => { | ||
// @ts-expect-error - TS2345 - Argument of type 'readonly MathFormat[] | undefined' is not assignable to parameter of type 'Collection<any>'. | ||
return _.map(answer.answerForms, (form) => { | ||
return (answer.answerForms || []).map((form) => { | ||
return { | ||
simplify: answer.simplify, | ||
name: form, | ||
}; | ||
}); | ||
}), | ||
), | ||
}); | ||
}; | ||
|
||
delete rendererProps.answers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of needing to jump through this typing hoop, what do you think of restructuring the answers
away?
const {answers: _, ...rendererProps} = {
...widgetOptions,
answerForms: unionAnswerForms(
widgetOptions.answers.map((answer) => {
return (answer.answerForms || []).map((form) => {
return {
simplify: answer.simplify,
name: form,
};
});
}),
),
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much better, thank you!
@@ -15,7 +16,7 @@ describe("generateExamples", () => { | |||
simplify: "required", | |||
}, | |||
]; | |||
const strings: Partial<PerseusStrings> = { | |||
const fakePerseusStrings: Partial<PerseusStrings> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: could we not use the mock strings from
perseus/packages/perseus/src/strings.ts
Line 551 in 9cabe68
export const mockStrings: PerseusStrings = { |
Summary:
This PR is part of the Numeric Input Project, and will be landed onto the feature branch for a full QA pass.
The intended goal was to remove all cases of underscore in the Numeric Input component, and to improve the commenting / documentation of the code.
Some things to note:
generateExamples
to matchshouldShowExamples
, as we were generating a list of all possible examples and simply not displaying it. This seemed unnecessary and we can exit both functions early.PerseusNumericInputWidgetOptions.simplify
Issue: LEMS-2446
Test plan: