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

[LEMS-2849] Answerless Radio #2233

Merged
merged 21 commits into from
Mar 5, 2025
Merged

[LEMS-2849] Answerless Radio #2233

merged 21 commits into from
Mar 5, 2025

Conversation

handeyeco
Copy link
Contributor

@handeyeco handeyeco commented Feb 12, 2025

Summary:

So this focuses on numCorrect which originally derived from the answers but now is its own widget option. I'm sure there's an argument that this also shouldn't be sent to the FE, but it's conditionally used to render help text: Select 2 answers. I don't know, I don't think it's that big of a deal to have it but I also made it optional so we can remove it from questions that don't need it: needsNumCorrect = options.multipleSelect === true && options.countChoices === true.

The important thing is that we can render, answer, and score a Radio widget that's been stripped of answers. As far as I can tell, this supports all behavior pre-scoring.

Issue: LEMS-2849

Test plan:

Radio should continue to be renderable, answerable, and scorable.

@handeyeco handeyeco self-assigned this Feb 12, 2025
Copy link
Contributor

github-actions bot commented Feb 12, 2025

Size Change: +591 B (+0.07%)

Total Size: 873 kB

Filename Size Change
packages/perseus-core/dist/es/index.js 30.5 kB +700 B (+2.35%)
packages/perseus-editor/dist/es/index.js 276 kB -118 B (-0.04%)
packages/perseus/dist/es/index.js 367 kB +9 B (0%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 39.7 kB
packages/keypad-context/dist/es/index.js 760 B
packages/kmath/dist/es/index.js 11.1 kB
packages/math-input/dist/es/index.js 78.2 kB
packages/math-input/dist/es/strings.js 1.79 kB
packages/perseus-linter/dist/es/index.js 22.8 kB
packages/perseus-score/dist/es/index.js 20.6 kB
packages/perseus/dist/es/strings.js 6.74 kB
packages/pure-markdown/dist/es/index.js 4.14 kB
packages/simple-markdown/dist/es/index.js 13.1 kB

compressed-size-action

@handeyeco handeyeco changed the title [LEMS-2849/radio-answerless] add broken test for answerful requirement Answerless Radio Feb 12, 2025
@handeyeco handeyeco changed the title Answerless Radio [LEMS-2849] Answerless Radio Feb 13, 2025
Comment on lines -109 to -108
const numCorrect: number = _.reduce(
widgetOptions.choices,
function (memo, choice) {
return choice.correct ? memo + 1 : memo;
},
0,
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main idea is to pull this logic out of transform and call it during the upgrade process. That way the widget can do all the pre-scoring things it needs to without answers (ie. render and be answerable).

// assert that functionality previous based on answers still works
expect(screen.getByRole("group", {name: "Choose 2 answers:"}));

const userInput = renderer.getUserInputMap();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is important because we can do everything we need to do pre-scoring with the answerless data now as far as I can tell.

@handeyeco handeyeco requested review from jeremywiebe, benchristel, Myranae and a team February 13, 2025 16:54
Copy link
Member

@benchristel benchristel left a comment

Choose a reason for hiding this comment

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

I left a few minor comments and suggestions inline, but nothing blocking. LGTM!

constant("radio"),
object({
numCorrect: optional(number),
Copy link
Member

Choose a reason for hiding this comment

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

This makes me slightly nervous. numCorrect only needs to be optional because we'll be running this parser on two different inputs:

  • data loaded from datastore, where numCorrect is usually not present (and maybe never needs to be present, since it can be derived?)
  • data sent to the the client, where numCorrect must be present if the data is answerless.

I worry that if the parsers accumulate a lot of irregularities of this kind, we'll be back in the position of not really understanding our data schema.

But I also think that it would be a pain in the butt to maintain separate parsers for answerless and answerful data.

One idea I'm toying with is letting the ParseContext keep track of whether we're parsing answerless or answerful data. Then we could write widget options parsers like this:

object({
  numCorrect: ifAnswerful(optional(number)).ifAnswerless(number),
})

But maybe that's overkill? I guess one thing I don't understand is whether the plan is to persist numCorrect in the answerful data or not.

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 NumericInput v2 numCorrect will:

  • always be in the data store
  • always be in the full widget options
  • will conditionally be in the "public" widget options

I added a commit since your comment that changes things. Maybe this is resolved now?

Copy link
Contributor

github-actions bot commented Feb 13, 2025

npm Snapshot: Published

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

Example:

pnpm add @khanacademy/perseus@PR2233

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

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

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.

Nice. I like the approach of removing countChoices in get getPublicWidgetOptions.

Comment on lines +1346 to +1348
// How many of the choices are correct, which is conditionally used to tell
// learners ahead of time how many options they'll need.
numCorrect?: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

Does updating this file require a stakeholder update? Apologies for pointing this out each time, but the memories of what Ned went through and Jeremy always talking about protecting this file are strong 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

numCorrect: PerseusRadioWidgetOptions["numCorrect"],
) {
return multipleSelect && countChoices && numCorrect;
}
Copy link
Member

@benchristel benchristel Feb 21, 2025

Choose a reason for hiding this comment

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

It's kind of unfortunate that this code is coupled to logic in the widget. We're predicting when the widget is going to need numCorrect, which feels brittle. EDIT: I see we're calling this function in the widget! In that case, I think it should be named something like revealNumCorrect or showNumCorrectInInstructions.

It seems like we could leverage the type system's ability to model the different possibilities here. Something like:

type PerseusRadioWidgetOptions = {
  responseFormat: SingleSelect | MultiSelect,
  // ...
}

type SingleSelect =  {type: "single-select"};

type MultiSelect = {
  type: "multi-select",
  /**
   * If present, the number of choices the learner must select for
   * their response to be accepted. Responses with a different number
   * of selected choices are marked invalid rather than incorrect. The
   * number of correct choices is displayed in the widget instructions.
   *
   * If null, the learner can submit a response with any number of 
   * selected options. Responses with the wrong number of options 
   * selected are marked incorrect.
   */
  numCorrect: number | null,
}

This would require a major version bump of the widget options format, so perhaps it's not something we want to take on right now. It might be worth migrating to this in the future. EDIT: ugh, sorry for the confusion. I was reading the diff of one commit thinking it was the whole PR. I see now that you are bumping the major version. In that case, I think changing to this more-structured format would be nice if we can make it work.

Comment on lines +217 to +224
* This was super annoying to figure out.
* When in "edit" mode (which the editor is obviously)
* the only way to select an option from the radio
* is to click the A/B/C/etc icons themselves.
* We have a `elem.getAttribute("data-is-radio-icon")` check
* to make sure that what we're clicking is the icon and
* not some other part of the radio choice.
* It's just a div, hence the test ID.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hopefully this can become alot simpler with the upcoming Radio rebasing on top of Wonder Blocks work.

Comment on lines 233 to 234
// now there should be 3 correct answers
expect(options?.numCorrect).toBe(3);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Stylistically, I find using an onChange jest.fn() for these types of things simpler to grok. With a var we set in the callback you have a to trace up through the test to understand that the onChange callback sets it.

With an onChange mock, we could use:

expect(onChange).toHaveBeenCalledWith(expect.objectContaining({
    numCorrect: 3
});

In my mind that's less logic to support the test than having a let options we manage. 🤷‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not a hill I'll die on, but I'm not totally sure I agree. I think...

expect(options?.numCorrect).toBe(3);

...is much more legible than...

expect(onChangeMock).toHaveBeenCalledWith(
    expect.objectContaining({
        numCorrect: 3,
    }),
);

I also think from a KISS perspective that a simple callback is easier to follow/debug than a Jest abstraction. But it's not a big deal in this particular case, so I made the change.

@handeyeco handeyeco merged commit a0aee41 into main Mar 5, 2025
8 checks passed
@handeyeco handeyeco deleted the LEMS-2849/radio-answerless branch March 5, 2025 18:27
handeyeco pushed a commit that referenced this pull request Mar 5, 2025
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @khanacademy/math-input@23.0.0

### Major Changes

- [#2226](#2226)
[`909148cdc`](909148c)
Thanks [@handeyeco](https://github.com/handeyeco)! - Answerless
Expression: Expression can render and is interactive with answerless
data

### Patch Changes

- Updated dependencies
\[[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus-core@5.0.0
    -   @khanacademy/keypad-context@1.1.1

## @khanacademy/perseus@56.0.0

### Major Changes

- [#2233](#2233)
[`a0aee41b6`](a0aee41)
Thanks [@handeyeco](https://github.com/handeyeco)! - RadioWidget v2 in
support of answerless Radio

### Patch Changes

- [#2279](#2279)
[`e02cc4109`](e02cc41)
Thanks [@nishasy](https://github.com/nishasy)! - [SR] Tweak strings for
Segment and Angle graphs


- [#2270](#2270)
[`941343ee3`](941343e)
Thanks [@catandthemachines](https://github.com/catandthemachines)! -
Fixing side snapping within polygons to be keyboard accessible.


- [#2226](#2226)
[`909148cdc`](909148c)
Thanks [@handeyeco](https://github.com/handeyeco)! - Answerless
Expression: Expression can render and is interactive with answerless
data

- Updated dependencies
\[[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus-core@5.0.0
    -   @khanacademy/math-input@23.0.0
    -   @khanacademy/keypad-context@1.1.1
    -   @khanacademy/kmath@0.4.1
    -   @khanacademy/perseus-linter@1.3.1
    -   @khanacademy/perseus-score@2.3.1

## @khanacademy/perseus-core@5.0.0

### Major Changes

- [#2233](#2233)
[`a0aee41b6`](a0aee41)
Thanks [@handeyeco](https://github.com/handeyeco)! - RadioWidget v2 in
support of answerless Radio


- [#2226](#2226)
[`909148cdc`](909148c)
Thanks [@handeyeco](https://github.com/handeyeco)! - Answerless
Expression: Expression can render and is interactive with answerless
data

## @khanacademy/perseus-editor@17.9.0

### Minor Changes

- [#2226](#2226)
[`909148cdc`](909148c)
Thanks [@handeyeco](https://github.com/handeyeco)! - Answerless
Expression: Expression can render and is interactive with answerless
data

### Patch Changes

- [#2233](#2233)
[`a0aee41b6`](a0aee41)
Thanks [@handeyeco](https://github.com/handeyeco)! - RadioWidget v2 in
support of answerless Radio

- Updated dependencies
\[[`e02cc4109`](e02cc41),
[`941343ee3`](941343e),
[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus@56.0.0
    -   @khanacademy/perseus-core@5.0.0
    -   @khanacademy/math-input@23.0.0
    -   @khanacademy/keypad-context@1.1.1
    -   @khanacademy/kmath@0.4.1
    -   @khanacademy/perseus-linter@1.3.1
    -   @khanacademy/perseus-score@2.3.1

## @khanacademy/keypad-context@1.1.1

### Patch Changes

- Updated dependencies
\[[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus-core@5.0.0

## @khanacademy/kmath@0.4.1

### Patch Changes

- Updated dependencies
\[[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus-core@5.0.0

## @khanacademy/perseus-linter@1.3.1

### Patch Changes

- Updated dependencies
\[[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus-core@5.0.0

## @khanacademy/perseus-score@2.3.1

### Patch Changes

- Updated dependencies
\[[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus-core@5.0.0
    -   @khanacademy/kmath@0.4.1

## @khanacademy/perseus-dev-ui@5.3.1

### Patch Changes

- Updated dependencies
\[[`a0aee41b6`](a0aee41),
[`909148cdc`](909148c)]:
    -   @khanacademy/perseus-core@5.0.0
    -   @khanacademy/math-input@23.0.0
    -   @khanacademy/kmath@0.4.1
    -   @khanacademy/perseus-linter@1.3.1

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.

4 participants