-
Notifications
You must be signed in to change notification settings - Fork 351
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
Add and pass regression tests for PerseusItem parser #1907
Conversation
GeraldRequired Reviewers
Don't want to be involved in this pull request? Comment |
Size Change: +159 B (+0.01%) Total Size: 1.29 MB
ℹ️ View Unchanged
|
@@ -1034,17 +1039,17 @@ export type PerseusMatcherWidgetOptions = { | |||
export type PerseusMatrixWidgetAnswers = ReadonlyArray<ReadonlyArray<number>>; | |||
export type PerseusMatrixWidgetOptions = { | |||
// Translatable Text; Shown before the matrix | |||
prefix: string; | |||
prefix?: string | undefined; |
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.
Doesn't the ?
make this optional (ie. making 'undefined' a legal "value")?
Seeing both ?
and undefined
feels like we're saying the same thing twice.
prefix?: string | undefined; | |
prefix?: string; |
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.
You're right. I thought prefix?: string
wouldn't allow the field to be explictly set to undefined
, but that appears not to be the case.
@@ -19,7 +20,7 @@ export const parseCategorizerWidget: Parser<CategorizerWidget> = parseWidget( | |||
items: array(string), | |||
categories: array(string), | |||
randomizeItems: boolean, | |||
static: boolean, | |||
static: defaulted(boolean, () => false), | |||
values: array(number), | |||
highlightLint: optional(boolean), | |||
linterContext: optional( |
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 definitely shouldn't be part of the serialized widget options for categorizer. It is a prop passed down by the editor components during content authoring. I can imagine this is in our published content because of a bug somewhere.
No action required, just musing.
import type {PerseusImageDetail} from "../../../perseus-types"; | ||
import type {Parser} from "../parser-types"; | ||
|
||
export const parseImages: Parser<{[key: string]: PerseusImageDetail}> = |
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.
Not a required change, but the keys in these image maps should be the url to the image. Clarifying the label may be helpful for others.
export const parseImages: Parser<{[key: string]: PerseusImageDetail}> = | |
export const parseImages: Parser<{[url: string]: PerseusImageDetail}> = |
// TODO(benchristel): content is also defaulted to empty string in | ||
// renderer.tsx. See if we can remove one default or the other. |
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've been thinking about this as I work through this PR. Some of our Perseus widgets/components provide default props. There are places we might be able to de-duplicate these defaults to only be defaulted in our parsers. But there may be some complications on the editing side as I'm pretty sure they depend on those default props.
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've been thinking about this too. I believe the correct place to add default values is in the parsing layer. That ensures that old content always renders consistently even if we change the default props used by the widget editors. We may still need defaultProps
in the widgets too, and I think that's okay.
For example, imagine we changed the default snapStep
for InteractiveGraph
from 0.5 to 1 in the widget's default props. If we relied on those default props to migrate legacy data that was missing snapStep
, the change would propagate to a lot of existing content — some of which might not be answerable with the new default snapStep
.
If, on the other hand, we defaulted snapStep
to 0.5 in the parsing layer, old content would be insulated from the change to the widget editor.
By defaulting values in the parser, we can decouple the "defaults for legacy content" from the "defaults for content creators".
@@ -19,7 +20,7 @@ export const parseRadioWidget: Parser<RadioWidget> = parseWidget( | |||
object({ | |||
choices: array( | |||
object({ | |||
content: string, | |||
content: defaulted(string, () => ""), |
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 defaulted()
would be easier to use if were defined such that the second parameter could be the literal default value as an option to a function that built the default value.
Something like this:
defaulted<T|Defaulted>(Parser<T>, Defaulted | () => Defaulted)
That'd allow many of these usages to be simpler, like this:
content: defaulted(string, () => ""), | |
content: defaulted(string, ""), |
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 danger with that approach is that if the default value is an object or array, the same instance will be reused for every default. E.g.
defaulted(array(someParser), [])
If some other code later mutates the array, it might affect unrelated code that happens to be holding a reference to the same value. This kind of "aliasing" bug has bitten me many times in my career, and it's always a nightmare.
I believe we have a pretty good culture of avoiding unnecessary mutation, but aliasing bugs are so bad that I feel it's better to be safe than sorry.
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.
Fair point. Let's leave it as a function then.
major: number, | ||
minor: number, |
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.
Related to other notes about defaults at different parts of Perseus, I wonder if we should make these defaulted() to 0
also? Ultimately, I suspect that the entire concept of versioning may go away because we'll just have "evergreen" parsers that handle all versions of content we've ever seen (and haven't backfilled). 🤔
No action needed, just an idea.
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.
Those defaults would let us handle data like version: {}
, right? I haven't seen any data like that. If we ever encountered it in the wild, I'd want to get a parse error rather than just silently converting it to {major: 0, minor: 0}
, because it would indicate that we have a bug somewhere else.
Evergreen parsers are a possibility, but I think we might sometimes want versioning to help make complex migrations typesafe and predictable. I do wish version
were just a number
; that would be a lot easier to deal with in TypeScript.
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.
Not for version: {}
but for items that have no version
key whatsoever.
oldWidgetInfo.version || {major: 0, minor: 0};
would only use the value after ||
if it is falsy, I believe.
@@ -100,10 +102,31 @@ export function getMatrixSize(matrix: ReadonlyArray<ReadonlyArray<number>>) { | |||
} | |||
|
|||
type ExternalProps = WidgetProps< | |||
PerseusMatrixWidgetOptions, | |||
{ |
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.
Is this necessary because the widget options defines static
as optional?
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.
It's needed because of the way TypeScript deals with React default props.
The Props
type argument to React.Component
represents the types of the props inside the component, after defaultProps
have been applied. However, we're using the *WidgetOptions
types to represent the props that are passed to the widget component. It's not correct to use *WidgetOptions
as the Props type argument if the component has default props.
The specific change that motivated this was that I made prefix
and suffix
optional on PerseusMatrixWidgetOptions
. I did that because prefix
and suffix
aren't always present in the JSON. But inside the component, they will always be strings, because they have defaultProps
.
Co-authored-by: Jeremy Wiebe <[email protected]>
d6697a2
to
b0ed10b
Compare
npm Snapshot: PublishedGood news!! We've packaged up the latest commit from this PR (7aca4b6) and published it to npm. You Example: yarn add @khanacademy/perseus@PR1907 If you are working in Khan Academy's webapp, you can run: ./dev/tools/bump_perseus_version.sh -t PR1907 |
Issue: LEMS-2582
Test plan:
yarn test