Skip to content
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

Create stories for Assessment's 5 scorable widgets that use answerless data #2329

Open
wants to merge 60 commits into
base: main
Choose a base branch
from

Conversation

Myranae
Copy link
Contributor

@Myranae Myranae commented Mar 26, 2025

Summary:

In order to confirm Assessment's main widgets can render with answerless data and then still score after that, we are adding stories for each of the widgets to test rendering them with answerless data.

Issue: LEMS-2869

Test plan:

  • Confirm all answerless stories start out with answerless data and then switch to answerful data in order to score.
  • Confirm Radio Controls work for answerful right away and for answerless after receiving answerful data.
  • Confirm the widgets in the stories score correctly.
  • Confirm story behavior has no regressions and is as expected.
  • Confirm all checks pass.

Myranae added 23 commits March 19, 2025 13:09
This is required for the check button to still work correctly
… data

It was using data without answers before. Although this is what we want, it's not how all the other answerless stories are
…enderer

Migrate dropdown stories to ServerItemRendererWithDebugUI instead of RendererWithDebugUI. Also renames the answerless stories and creates PerseusItems from the previously available question objects.
…eusRenderers

Some of the test data is also used in tests, so new PerseusItem objects were created in addition to the PerseusRenderer objects for those.
…emRenderer

ServerItemRenderer requires PerseusItem objects whereas Renderer just requires PerseusRenderer. Creating PerseusItem objects from the available data for Interactive Graph widget
…ServerItemRenderer

Also rename the stories for consistency and ease of locating in storybook.
…erless stories

Update the stories to use ServerItemRenderWithDebugUI. Since we need answerless stories to use this, we have to update all the stories since they all use the same render logic.
…tories where Dropdown, Interactive Graph, Numeric Input, Expression, and Radio widgets render using answerless data
…ithDebugUI

Updates the numeric-input answerless story specifically
@Myranae Myranae self-assigned this Mar 26, 2025
Copy link
Contributor

github-actions bot commented Mar 26, 2025

Size Change: 0 B

Total Size: 734 kB

ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 39.1 kB
packages/keypad-context/dist/es/index.js 760 B
packages/kmath/dist/es/index.js 10.5 kB
packages/math-input/dist/es/index.js 77.1 kB
packages/math-input/dist/es/strings.js 1.79 kB
packages/perseus-core/dist/es/index.js 31.7 kB
packages/perseus-editor/dist/es/index.js 135 kB
packages/perseus-linter/dist/es/index.js 22.3 kB
packages/perseus-score/dist/es/index.js 20.7 kB
packages/perseus-utils/dist/es/index.js 809 B
packages/perseus/dist/es/index.js 371 kB
packages/perseus/dist/es/strings.js 6.72 kB
packages/pure-markdown/dist/es/index.js 3.58 kB
packages/simple-markdown/dist/es/index.js 12.6 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Mar 26, 2025

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (b49e6bd) and published it to npm. You
can install it using the tag PR2329.

Example:

pnpm add @khanacademy/perseus@PR2329

If you are working in Khan Academy's webapp, you can run:

./dev/tools/bump_perseus_version.sh -t PR2329

Copy link
Collaborator

@jeremywiebe jeremywiebe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these. I have a few comments, but nothing major. :)


// Data for the interactive graph widget

const createInteractiveGraphItem = (question: PerseusRenderer): PerseusItem => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems similar to the createDropdownItem. Could we have a single createPerseusItem(question: PerseusRenderer) instead?

@@ -180,6 +182,29 @@ const updateWidgetOptions = (
};
};

const createNumericInputItem = (question: PerseusRenderer): PerseusItem => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note as above about unifying this with the other createXyzItem functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions should now be gone in favor of using the previously created utility that Matthew pointed out.

Comment on lines -211 to -217
images: {
"web+graphie://ka-perseus-graphie.s3.amazonaws.com/4613e0d9c906b3053fb5523eed83d4f779fdf6bb":
{
width: 428,
height: 428,
},
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For test data and stories this is likely ok. The images key in a perseus renderer object is used to explicitly set the width and the height of an image (using the image URL as the key into images).

// We need to add width and height to images from our
// props.images mapping.
// We do a _.has check here to avoid weird things like
// 'toString' or '__proto__' as a url.
const extraAttrs = _.has(this.props.images, node.target)
? this.props.images[node.target]
: null;

Comment on lines 4 to 32
* Creates a Perseus item with the provided question/PerseusRenderer data.
* Useful for converting test data used with RendererWithDebugUI to test data
* for ServerItemRendererWithDebugUI.
*
* @param {PerseusRenderer} question - The question to be included in the Perseus item.
* @returns {PerseusItem} The created Perseus item.
*/
export const createPerseusItem = (question: PerseusRenderer): PerseusItem => {
return {
question: question,
answer: null,
itemDataVersion: {
major: 0,
minor: 1,
},
hints: [],
answerArea: {
calculator: false,
chi2Table: false,
financialCalculatorMonthlyPayment: false,
financialCalculatorTotalAmount: false,
financialCalculatorTimeToPayOff: false,
periodicTable: false,
periodicTableWithKey: false,
tTable: false,
zTable: false,
},
};
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeremywiebe How does this look for a shared test util? I think I put it in a good place, but wasn't sure on the file naming. Also, tbh, tried having copilot write the doc string. Not sure the parameter and return needs to be listed in the comment 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have generateTestPerseusItem in the perseus package. LEMS needs to devote a sprint or two to combining our test utils.

Also might be worth looking into how Ned handled answerArea:

    answerArea: Object.fromEntries(
        ItemExtras.map((extra) => [extra, false]),
    ) as PerseusAnswerArea,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this also points to something Matthew brought up to me in conversation recently, that we have the ServerItemRenderer UI and Renderer UI... they overlap alot and I think we could get away with only the Renderer UI component. I'll look into that this week yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yea when I talked about this with Matthew, we thought we were leaning towards keeping the ServerItemRenderer and moving away from the Renderer one. We can definitely make things work either way, but that's why the answerless stories are using the ServerItemRenderer component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this duplicate utility function creation and used the one that was already defined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we thought we were leaning towards keeping the ServerItemRenderer and moving away from the Renderer one

That would be my vote anyway, let's get close to what consumers are mostly using which is ServerItemRenderer

"@khanacademy/perseus": minor
---

Update and add stories where Dropdown, Interactive Graph, Numeric Input, Expression, and Radio widgets render using answerless data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future PRs, it would be beneficial to have separate PRs. In this case it could have been 5 PRs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure. I thought each was small enough to be combined into one, but I can still separate them out if you think that would be useful?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main goal (IMO) of small PRs is to make review easier. Since you already have the reviews, I don't think it'd be worth the trouble.

Comment on lines 4 to 32
* Creates a Perseus item with the provided question/PerseusRenderer data.
* Useful for converting test data used with RendererWithDebugUI to test data
* for ServerItemRendererWithDebugUI.
*
* @param {PerseusRenderer} question - The question to be included in the Perseus item.
* @returns {PerseusItem} The created Perseus item.
*/
export const createPerseusItem = (question: PerseusRenderer): PerseusItem => {
return {
question: question,
answer: null,
itemDataVersion: {
major: 0,
minor: 1,
},
hints: [],
answerArea: {
calculator: false,
chi2Table: false,
financialCalculatorMonthlyPayment: false,
financialCalculatorTotalAmount: false,
financialCalculatorTimeToPayOff: false,
periodicTable: false,
periodicTableWithKey: false,
tTable: false,
zTable: false,
},
};
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have generateTestPerseusItem in the perseus package. LEMS needs to devote a sprint or two to combining our test utils.

Also might be worth looking into how Ned handled answerArea:

    answerArea: Object.fromEntries(
        ItemExtras.map((extra) => [extra, false]),
    ) as PerseusAnswerArea,

): React.ReactElement => {
return (
<ServerItemRendererWithDebugUI
item={basicDropdownItem}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a little overkill to duplicate each story (really we just needed one interactive answerless story per widget). Having a bunch of stories that are nearly identical might be confusing to people especially in a couple of months when all stories should start answerless. Not too worried about this, but maybe we need a post-SSS ticket to come back and simplify the stories.

What I think is definitely overkill is exporting/importing what is essentially the same test data. Rather than redeclare the test data, just wrap the existing test data.

Suggested change
item={basicDropdownItem}
item={createPerseusItem(basicDropdownItem)}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the stories to do inline item creation and reduced the overall number of stories, though I kept one for each type of interactive graph.

@@ -29,6 +35,8 @@ export const angleQuestion: PerseusRenderer = interactiveGraphQuestionBuilder()
})
.build();

export const angleItem: PerseusItem = createPerseusItem(angleQuestion);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we don't need to be redeclaring a bunch of test data when we could just be wrapping the existing test data in createPerseusItem

@@ -0,0 +1,90 @@
// NOTE(Tamara): ServerItemRenderWithDebugUI does not work with the same props
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than make a new file, we need to update radio.stories.tsx to use ServerItemRendererWithDebugUI. The problem isn't that we need stories for both ServerItemRendererWithDebugUI and RendererWithDebugUI, the problem is that RendererWithDebugUI needs to be deprecated and replaced with ServerItemRendererWithDebugUI

Copy link
Contributor Author

@Myranae Myranae Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to preserve Controls functionality for the answerful stories and wasn't sure how to do that with ServerItemRendererWithDebugUI. So I changed it from converting all stories to ServerItem to just the answerless stories in the new file. Just took a second attempt and was able to preserve the Controls functionality for all stories. The answerless ones just need to turn startAnswerless off or click the Check button first to get full data. Removed this additional Radio stories page.

Comment on lines -211 to -217
images: {
"web+graphie://ka-perseus-graphie.s3.amazonaws.com/4613e0d9c906b3053fb5523eed83d4f779fdf6bb":
{
width: 428,
height: 428,
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This style issue is unrelated to LEMS-2948, likely just a problem with styling in the fake Perseus Chrome we use in SB. Removing the image only sweeps the issue under the rug, we need a separate ticket to look into the styles causing this.

@@ -126,6 +129,9 @@ export const questionWithPassage: PerseusRenderer = {
},
};

export const questionWithPassageItem: PerseusItem =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, don't need a new declaration.

@@ -15,7 +15,7 @@ import * as Perseus from "../packages/perseus/src/index";
import {keScoreFromPerseusScore} from "../packages/perseus/src/util/scoring";

import KEScoreUI from "./ke-score-ui";
import SideBySide from "./split-view";
import SplitView from "./split-view";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Comment on lines 3 to 10
/**
* Creates a Perseus item with the provided question/PerseusRenderer data.
* Useful for converting test data used with RendererWithDebugUI to test data
* for ServerItemRendererWithDebugUI.
*
* @param {PerseusRenderer} question - The question to be included in the Perseus item.
* @returns {PerseusItem} The created Perseus item.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding docs. For things that are self-evident, I prefer to avoid adding a doc comment. So I think I'd just recommend this (as the question param is quite obvious as is the return value):

Suggested change
/**
* Creates a Perseus item with the provided question/PerseusRenderer data.
* Useful for converting test data used with RendererWithDebugUI to test data
* for ServerItemRendererWithDebugUI.
*
* @param {PerseusRenderer} question - The question to be included in the Perseus item.
* @returns {PerseusItem} The created Perseus item.
*/
/**
* Creates a Perseus item with the provided question/PerseusRenderer data.
* Useful for converting test data used with RendererWithDebugUI to test data
* for ServerItemRendererWithDebugUI.
*/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Ended up removing this file, but glad to know my thought to remove the parameter definitions was correct.

Copy link
Contributor

@handeyeco handeyeco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only blocker is the change to ServerItemRenderer, otherwise it's looking good

@@ -46,7 +46,7 @@ type OwnProps = {
item: PerseusItem;
score?: KEScore | null;
problemNum?: number;
reviewMode?: boolean;
reviewMode?: boolean | null | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this change. It's a change to our external API (ServerItemRenderer is production code, not test code), so I think this needs to be reverted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure! That makes sense. Instead, I updated the props for ServerItemRendererWithDebugUI to be in alignment with reviewMode?: boolean;.

Comment on lines 23 to 24
// Renderer Options
startAnswerless: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more "Testing Options" because this flag doesn't actually get passed down to the renderer, it just affects the code that gets passed to the renderer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

Comment on lines 114 to 119
// NOTE(Tamara): The answerless radio stories currently lose the user's input
// upon switching to answerless data initially. It will remember the user's
// input after clicking the Check button a second time.
// TODO(LEMS-2948): After investigating a solution, confirm this issue is fixed

export const AnswerlessSingleSelect = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// NOTE(Tamara): The answerless radio stories currently lose the user's input
// upon switching to answerless data initially. It will remember the user's
// input after clicking the Check button a second time.
// TODO(LEMS-2948): After investigating a solution, confirm this issue is fixed
export const AnswerlessSingleSelect = {
// TODO(LEMS-2948): The answerless radio stories currently lose the user's input
// upon switching to answerless data initially. It loses the user's
// input after clicking the Check button a second time.
export const AnswerlessSingleSelect = {

I thought we confirmed that it doesn't remember the user input when clicking Check twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the way I wrote this was confusing. I updated the comment. User input is lost with the first click. If the user selects an option after that and then clicks Check again, it will remember the input. It's just that first time when it switches to answerful data.

Comment on lines 32 to 33
reviewMode?: boolean | null | undefined;
showSolutions?: ShowSolutions;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing you added this to verify that rationales/solutions/etc still work; I think that's a great idea.

The problem is that those, by definition, require answerful data. We should never be in a state where static, reviewMode, or showSolutions are true while using answerless data.

🤔 I think short-term maybe we could add some semi-hacky but well commented code to make this work:

    // `reviewMode` and `showSolutions` require answerful data by definition,
    // so only use answerless data if those are not enabled
    const shouldUseAnswerless = useAnswerless && !reviewMode && showSolutions === "none";
    const renderedQuestion: PerseusRenderer = shouldUseAnswerless
        ? splitPerseusItem(item.question)
        : item.question;

It might also be beneficial to let people who are using the stories know which version of the data they're rendering with, but that could be a small follow-up ticket.

Copy link
Contributor Author

@Myranae Myranae Mar 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works with Radio because showSolutions is undefined with other widgets. I updated it a bit so that it works with other widgets and also so that the startAnswerless toggle actually toggles between the data! Not sure how to make the static toggle do this though since it's not in the props and seems to be added by default. Would love to change startAnswerless to answerless and have the toggle update based on the status of the other toggles and the answerless status of the question.

@@ -79,10 +88,13 @@ export const ServerItemRendererWithDebugUI = ({
item={renderedItem}
dependencies={storybookDependenciesV2}
keypadElement={keypadElement}
reviewMode={reviewMode}
{...rest}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a personal nit, but I try to avoid ...rest for component props because I think it obfuscates usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure. Pulled out the last prop I know was needed and removed the rest spread.

Copy link
Contributor

@handeyeco handeyeco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startAnswerless should be removed from the shouldUseAnswerless, but otherwise looks good

Comment on lines 70 to 79
// `reviewMode` and `showSolutions` require answerful data by definition,
// so only use answerless data if those are not enabled. Also updates the
// startAnswerless toggle so it actually switches between answerless and answerful.
const shouldUseAnswerless =
useAnswerless &&
!reviewMode &&
(showSolutions === "none" || !showSolutions) &&
startAnswerless;

const renderedQuestion: PerseusRenderer = shouldUseAnswerless
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// `reviewMode` and `showSolutions` require answerful data by definition,
// so only use answerless data if those are not enabled. Also updates the
// startAnswerless toggle so it actually switches between answerless and answerful.
const shouldUseAnswerless =
useAnswerless &&
!reviewMode &&
(showSolutions === "none" || !showSolutions) &&
startAnswerless;
const renderedQuestion: PerseusRenderer = shouldUseAnswerless
// `reviewMode` and `showSolutions` require answerful data by definition,
// so only use answerless data if those are not enabled.
const shouldUseAnswerless =
useAnswerless &&
!reviewMode &&
(showSolutions === "none" || !showSolutions);
const renderedQuestion: PerseusRenderer = shouldUseAnswerless

You don't want to directly use startAnswerless because useAnswerless is the stateful version of startAnswerless. This also doesn't update startAnswerless so the comment is a little confusing.

Just realizing useAnswerless should probably be renamed so it's not confused with a hook 🤦 That was my bad. Maybe just answerless/setAnswerless or something. This is getting a little confusing, probably due to bad design decisions on my part...but it's test code, so we can always fix it later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@handeyeco Okay, I renamed startAnswerless to answerless/setAnswerless.

Regarding startAnswerless being there (on L77 of the highlighted code), when it is there, toggling startAnswerless in the Controls for answerless stories immediately changes the data from answerless to answerful. It actually acts as a functioning toggle. It doesn't seem to work if startAnswerless starts out being false though. Those need a page refresh. Should I still take startAnswerless out from here though?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants