diff --git a/.changeset/nervous-moons-roll.md b/.changeset/nervous-moons-roll.md new file mode 100644 index 00000000000..a1f1b8e1bcd --- /dev/null +++ b/.changeset/nervous-moons-roll.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Convert font and colors to semantic tokens for label image diff --git a/packages/perseus/src/widgets/label-image/__docs__/label-image-initial-state-regression.stories.tsx b/packages/perseus/src/widgets/label-image/__docs__/label-image-initial-state-regression.stories.tsx new file mode 100644 index 00000000000..60048da9ab2 --- /dev/null +++ b/packages/perseus/src/widgets/label-image/__docs__/label-image-initial-state-regression.stories.tsx @@ -0,0 +1,56 @@ +import {generateTestPerseusItem} from "@khanacademy/perseus-core"; + +import {themeModes} from "../../../../../../.storybook/modes"; +import {ServerItemRendererWithDebugUI} from "../../../testing/server-item-renderer-with-debug-ui"; +import { + incorrectAnswerQuestion, + textQuestion, + numberline, +} from "../__tests__/label-image.testdata"; + +import type {Meta, StoryObj} from "@storybook/react-vite"; + +const meta: Meta = { + title: "Widgets/Label Image/Visual Regression Tests/Initial State", + component: ServerItemRendererWithDebugUI, + tags: ["!dev"], + parameters: { + docs: { + description: { + component: + "Regression tests for the Label Image widget that do NOT " + + "need any interactions to test.", + }, + }, + chromatic: {disableSnapshot: false, modes: themeModes}, + }, +}; +export default meta; + +type Story = StoryObj; + +// Verifies the default unanswered state: all markers visible and pulsating, +// no answers selected, text choices. +export const DefaultUnanswered: Story = { + args: { + item: generateTestPerseusItem({question: textQuestion}), + }, +}; + +// Verifies choices shown in the instructions section (hideChoicesFromInstructions: false), +// including TeX fraction choices and the rgba(33, 36, 44, 0.32) separator dots +// that appear between each choice. +export const WithChoicesInInstructions: Story = { + args: { + item: generateTestPerseusItem({question: numberline}), + }, +}; + +// Verifies the incorrect marker state: marker dot renders with neutral +// background (background.neutral.default) when showCorrectness is "incorrect". +// No answer pill is shown because no answer is selected in this static state. +export const IncorrectMarker: Story = { + args: { + item: generateTestPerseusItem({question: incorrectAnswerQuestion}), + }, +}; diff --git a/packages/perseus/src/widgets/label-image/__docs__/label-image-interactions-regression.stories.tsx b/packages/perseus/src/widgets/label-image/__docs__/label-image-interactions-regression.stories.tsx new file mode 100644 index 00000000000..a6c98f189c8 --- /dev/null +++ b/packages/perseus/src/widgets/label-image/__docs__/label-image-interactions-regression.stories.tsx @@ -0,0 +1,125 @@ +import {generateTestPerseusItem} from "@khanacademy/perseus-core"; +import {within} from "storybook/test"; + +import {themeModes} from "../../../../../../.storybook/modes"; +import {ServerItemRendererWithDebugUI} from "../../../testing/server-item-renderer-with-debug-ui"; +import { + incorrectAnswerQuestion, + mathQuestion, + shortTextQuestion, + textQuestion, +} from "../__tests__/label-image.testdata"; + +import type {Meta} from "@storybook/react-vite"; + +const meta: Meta = { + title: "Widgets/Label Image/Visual Regression Tests/Interactions", + component: ServerItemRendererWithDebugUI, + tags: ["!autodocs"], + parameters: { + chromatic: {disableSnapshot: false, modes: themeModes}, + }, +}; + +export default meta; + +// Verifies the marker open/selected state: clicking a marker button opens the +// answer choices dropdown and shows the active marker styling. +export const MarkerOpened = { + args: { + item: generateTestPerseusItem({question: textQuestion}), + }, + play: async ({canvasElement, userEvent}) => { + const canvas = within(canvasElement); + const marker = canvas.getByLabelText("The fourth unlabeled bar line."); + await userEvent.click(marker); + }, +}; + +// Verifies the post-interaction marker state: after selecting an answer and +// closing the dropdown, all markers render as white circles (not the default +// pulsing blue). +export const AnswerSelected = { + args: { + item: generateTestPerseusItem({question: textQuestion}), + }, + play: async ({canvasElement, userEvent}) => { + const canvas = within(canvasElement); + const marker = canvas.getByLabelText("The fourth unlabeled bar line."); + await userEvent.click(marker); + + // WonderBlocks SingleSelect renders options into a React portal outside + // the canvas, so we scope to document.body. + const suvsChoice = within(document.body).getByRole("option", { + name: "SUVs", + }); + await userEvent.click(suvsChoice); + }, +}; + +// Verifies the correct answer state: after selecting the right answer and +// clicking Check, the marker and answer pill render in green (success.strong). +// Uses shortTextQuestion (single marker) to avoid needing to fill all markers. +// Check is clicked twice due to a server-side scoring quirk in Storybook. +export const CorrectAnswerGraded = { + args: { + item: generateTestPerseusItem({question: shortTextQuestion}), + }, + play: async ({canvasElement, userEvent}) => { + const canvas = within(canvasElement); + + const marker = canvas.getByLabelText("The fourth unlabeled bar line."); + await userEvent.click(marker); + + // WonderBlocks SingleSelect renders options into a React portal outside + // the canvas, so we scope to document.body. + const suvsChoice = within(document.body).getByRole("option", { + name: "SUVs", + }); + await userEvent.click(suvsChoice); + + // eslint-disable-next-line testing-library/prefer-screen-queries + const checkButton = canvas.getByRole("button", {name: "Check answer"}); + await userEvent.click(checkButton); + await userEvent.click(checkButton); + }, +}; + +// Verifies the incorrect answer state: marker dot renders with neutral +// background (background.neutral.default) and the answer pill shows the wrong +// selection with the same neutral styling. Uses incorrectAnswerQuestion, which +// has showCorrectness "incorrect" pre-set on the marker in the question data, +// matching how this state is passed in from review/show-solutions contexts. +// The play function selects an answer so the pill becomes visible. +export const IncorrectAnswerWithPill = { + args: { + item: generateTestPerseusItem({question: incorrectAnswerQuestion}), + }, + play: async ({canvasElement, userEvent}) => { + const canvas = within(canvasElement); + + const marker = canvas.getByLabelText("The fourth unlabeled bar line."); + await userEvent.click(marker); + + // WonderBlocks SingleSelect renders options into a React portal outside + // the canvas, so we scope to document.body. + const trucksChoice = within(document.body).getByRole("option", { + name: "Trucks", + }); + await userEvent.click(trucksChoice); + }, +}; + +// Verifies that math choices render correctly inside an open marker dropdown. +// The math choices are only visible after opening a marker, so we capture +// the open state here. +export const MathChoicesVisible = { + args: { + item: generateTestPerseusItem({question: mathQuestion}), + }, + play: async ({canvasElement, userEvent}) => { + const canvas = within(canvasElement); + const marker = canvas.getByLabelText("The fourth unlabeled bar line."); + await userEvent.click(marker); + }, +}; diff --git a/packages/perseus/src/widgets/label-image/__tests__/label-image.testdata.ts b/packages/perseus/src/widgets/label-image/__tests__/label-image.testdata.ts index 524e1c8691c..613736e2d1b 100644 --- a/packages/perseus/src/widgets/label-image/__tests__/label-image.testdata.ts +++ b/packages/perseus/src/widgets/label-image/__tests__/label-image.testdata.ts @@ -223,6 +223,54 @@ export const numberline: PerseusRenderer = { }, }; +// Not typed as PerseusRenderer because showCorrectness is part of +// InteractiveMarkerType (runtime UI state) but not PerseusLabelImageMarker +// (the schema type). The field is supported by the widget component at runtime. +export const incorrectAnswerQuestion = { + content: + "Carol created a chart and a bar graph to show how many of each type of vehicle were in her supermarket parking lot.\n\nVehicle Type | Number in the parking lot\n:- | :-: \nTrucks| $25$ \nVans | $5$ \nCars| $40$ \nSUVs | $10$ \n\n**Label each bar on the bar graph.**\n\n[[☃ label-image 1]]\n\n", + images: { + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/1e28332fd2321975639ab50c9ce442e568c18421": + { + width: 341, + height: 310, + }, + }, + widgets: { + "label-image 1": { + type: "label-image" as const, + alignment: "default", + static: false, + graded: true, + options: { + static: false, + choices: ["Trucks", "Vans", "Cars", "SUVs"], + imageAlt: + "A bar graph with four bar lines that shows the horizontal axis labeled Number in the parking lot and the vertical axis labeled Vehicle Type. The horizontal axis is labeled, from left to right: 0, 10, 20, 30, 40, and 50. The vertical axis has, from the bottom to the top, four unlabeled bar lines as follows: the first unlabeled bar line extends to the middle of 0 and 10, the second unlabeled bar line extends to 40, the third unlabeled bar line extends to the middle of 20 and 30, and fourth unlabeled bar line extends to 10.", + imageUrl: + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/56c60c72e96cd353e4a8b5434506cd3a21e717af", + imageWidth: 415, + imageHeight: 314, + markers: [ + { + answers: ["SUVs"], + label: "The fourth unlabeled bar line.", + x: 25, + y: 17.7, + showCorrectness: "incorrect", + }, + ], + multipleAnswers: false, + hideChoicesFromInstructions: true, + }, + version: { + major: 0, + minor: 0, + }, + }, + }, +}; + export const longTextFromArticle: PerseusRenderer = { content: "[[☃ label-image 1]]", images: {}, diff --git a/packages/perseus/src/widgets/label-image/answer-pill.tsx b/packages/perseus/src/widgets/label-image/answer-pill.tsx index 348d0736b79..6c561a039e1 100644 --- a/packages/perseus/src/widgets/label-image/answer-pill.tsx +++ b/packages/perseus/src/widgets/label-image/answer-pill.tsx @@ -85,8 +85,7 @@ export const AnswerPill = (props: { const styles = StyleSheet.create({ correct: { - // WB green darkened by 18% - backgroundColor: "#00880b", + backgroundColor: semanticColor.core.background.success.strong, }, incorrect: { backgroundColor: semanticColor.core.background.neutral.default, diff --git a/packages/perseus/src/widgets/label-image/label-image.tsx b/packages/perseus/src/widgets/label-image/label-image.tsx index f4559f23881..232477074eb 100644 --- a/packages/perseus/src/widgets/label-image/label-image.tsx +++ b/packages/perseus/src/widgets/label-image/label-image.tsx @@ -9,6 +9,7 @@ import {scoreLabelImageMarker} from "@khanacademy/perseus-score"; import Clickable from "@khanacademy/wonder-blocks-clickable"; import {View} from "@khanacademy/wonder-blocks-core"; +import {semanticColor} from "@khanacademy/wonder-blocks-tokens"; import {StyleSheet, css} from "aphrodite"; import classNames from "classnames"; import * as React from "react"; @@ -862,7 +863,7 @@ const styles = StyleSheet.create({ marginLeft: 5, marginRight: 5, - background: "rgba(33, 36, 44, 0.32)", + background: semanticColor.core.border.neutral.default, borderRadius: 2, }, diff --git a/packages/perseus/src/widgets/label-image/marker.tsx b/packages/perseus/src/widgets/label-image/marker.tsx index c151c39b3c0..bf83a4747ce 100644 --- a/packages/perseus/src/widgets/label-image/marker.tsx +++ b/packages/perseus/src/widgets/label-image/marker.tsx @@ -265,7 +265,7 @@ const styles = StyleSheet.create({ // The learner has made a selection markerFilled: { - backgroundColor: "#ECF3FE", + backgroundColor: semanticColor.core.background.instructive.subtle, border: `4px solid ${semanticColor.core.border.instructive.default}`, }, @@ -279,7 +279,7 @@ const styles = StyleSheet.create({ }, markerCorrect: { - background: "#00880b", // WB green darkened by 18% + background: semanticColor.core.background.success.strong, }, markerIncorrect: {