Repository: Khan/perseus
Pull request #2495: Update label image widget to handle answerless data and do some cleanup
Author: login: Myranae
Labels: name: olc-5.0.f1826
Created: 2025-05-15T22:31:58Z | Merged: 2025-05-22T16:53:56Z

## Summary:
There are two main methods of Label Image that use answers: showRationalesForCurrentlySelectedChoices and renderMarkers. Both were updated to handle answerless data, though the first one is mainly used after scoring has begun and should have access to answers already. Additional tests were added, props were updated to account for answerless data, and an answerless story was added.

Issue: LEMS-2981

## Test plan:
- Confirm all checks and tests pass
- `pnpm storybook`: Confirm widget renders, is interactive, and scores as expected for both answerless and answerful stories


Diff:
diff --git a/.changeset/green-yaks-scream.md b/.changeset/green-yaks-scream.md
new file mode 100644
index 00000000000..548c4fb188f
--- /dev/null
+++ b/.changeset/green-yaks-scream.md
@@ -0,0 +1,6 @@
+---
+"@khanacademy/perseus": minor
+"@khanacademy/perseus-core": minor
+---
+
+Update label image widget to handle answerless data and do some cleanup
diff --git a/packages/perseus-core/src/index.ts b/packages/perseus-core/src/index.ts
index 7439f3a9b8b..cd15a345468 100644
--- a/packages/perseus-core/src/index.ts
+++ b/packages/perseus-core/src/index.ts
@@ -159,6 +159,8 @@ export {
     default as getSorterPublicWidgetOptions,
     shuffleSorter,
 } from "./widgets/sorter/sorter-util";
+export type {LabelImagePublicWidgetOptions} from "./widgets/label-image/label-image-util";
+export type {LabelImageMarkerPublicData} from "./widgets/label-image/label-image-util";
 export {default as getDropdownPublicWidgetOptions} from "./widgets/dropdown/dropdown-util";
 export type {DropdownPublicWidgetOptions} from "./widgets/dropdown/dropdown-util";
 export {default as getNumericInputPublicWidgetOptions} from "./widgets/numeric-input/numeric-input-util";
diff --git a/packages/perseus-core/src/widgets/label-image/label-image-util.ts b/packages/perseus-core/src/widgets/label-image/label-image-util.ts
index 14374b9f37b..7d974d0a1d4 100644
--- a/packages/perseus-core/src/widgets/label-image/label-image-util.ts
+++ b/packages/perseus-core/src/widgets/label-image/label-image-util.ts
@@ -7,7 +7,7 @@ import type {
  * For details on the individual options, see the
  * PerseusLabelImageWidgetOptions type
  */
-type LabelImagePublicWidgetOptions = {
+export type LabelImagePublicWidgetOptions = {
     choices: PerseusLabelImageWidgetOptions["choices"];
     imageUrl: PerseusLabelImageWidgetOptions["imageUrl"];
     imageAlt: PerseusLabelImageWidgetOptions["imageAlt"];
@@ -19,7 +19,7 @@ type LabelImagePublicWidgetOptions = {
     static: PerseusLabelImageWidgetOptions["static"];
 };
 
-type LabelImageMarkerPublicData = Pick<
+export type LabelImageMarkerPublicData = Pick<
     PerseusLabelImageMarker,
     "x" | "y" | "label"
 >;
diff --git a/packages/perseus-score/src/widgets/label-image/score-label-image.ts b/packages/perseus-score/src/widgets/label-image/score-label-image.ts
index 6afbc150243..5c0b32b8c17 100644
--- a/packages/perseus-score/src/widgets/label-image/score-label-image.ts
+++ b/packages/perseus-score/src/widgets/label-image/score-label-image.ts
@@ -5,7 +5,7 @@ import type {
 } from "@khanacademy/perseus-core";
 
 // Question state for marker as result of user selected answers.
-type InteractiveMarkerScore = {
+export type InteractiveMarkerScore = {
     // Whether user selected answers for the marker.
     hasAnswers: boolean;
     // Whether user (answer) selection answered the question correctly.
diff --git a/packages/perseus/src/types.ts b/packages/perseus/src/types.ts
index ee809a8f228..ba36589e276 100644
--- a/packages/perseus/src/types.ts
+++ b/packages/perseus/src/types.ts
@@ -26,6 +26,8 @@ import type {
     UserInputArray,
     UserInputMap,
     Relationship,
+    LabelImageMarkerPublicData,
+    PerseusLabelImageMarker,
 } from "@khanacademy/perseus-core";
 import type {LinterContextProps} from "@khanacademy/perseus-linter";
 import type {Result} from "@khanacademy/wonder-blocks-data";
@@ -150,6 +152,8 @@ export type ChangeHandler = (
         numLinePosition?: number;
         // widgets/number-line.ts
         rel?: Relationship;
+        // widgets/label-image.tsx
+        markers?: Array<LabelImageMarkerPublicData | PerseusLabelImageMarker>;
     },
     callback?: () => void,
     silent?: boolean,
diff --git a/packages/perseus/src/widgets/label-image/__stories__/label-image.stories.tsx b/packages/perseus/src/widgets/label-image/__stories__/label-image.stories.tsx
index 96677958578..a20fde7a19e 100644
--- a/packages/perseus/src/widgets/label-image/__stories__/label-image.stories.tsx
+++ b/packages/perseus/src/widgets/label-image/__stories__/label-image.stories.tsx
@@ -100,6 +100,17 @@ export const LabelImageMixedContent = (args: StoryArgs): React.ReactElement => {
     );
 };
 
+export const LabelWidgetAnswerless = (args: StoryArgs): React.ReactElement => {
+    return (
+        <ServerItemRendererWithDebugUI
+            item={generateTestPerseusItem({
+                question: applyStoryArgs(textQuestion, args),
+            })}
+            startAnswerless={true}
+        />
+    );
+};
+
 export default {
     title: "Perseus/Widgets/Label Image",
     args: {
diff --git a/packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts b/packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
index 7fde3d48f22..8bc701855f7 100644
--- a/packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
+++ b/packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
@@ -1,3 +1,8 @@
+import {
+    generateTestPerseusItem,
+    splitPerseusItem,
+} from "@khanacademy/perseus-core";
+import {scorePerseusItem} from "@khanacademy/perseus-score";
 import {screen} from "@testing-library/react";
 import {userEvent as userEventLib} from "@testing-library/user-event";
 
@@ -10,15 +15,13 @@ import {scorePerseusItemTesting} from "../../../util/test-utils";
 import {renderQuestion} from "../../__testutils__/renderQuestion";
 import {LabelImage} from "../label-image";
 
-import {
-    shortTextQuestion,
-    textQuestion,
-    textWithoutAnswersQuestion,
-} from "./label-image.testdata";
+import {shortTextQuestion, textQuestion} from "./label-image.testdata";
 
+import type {OptionalAnswersMarkerType} from "../label-image";
+import type {InteractiveMarkerType} from "@khanacademy/perseus-core";
 import type {UserEvent} from "@testing-library/user-event";
 
-const emptyMarker = {
+const emptyMarker: InteractiveMarkerType = {
     label: "",
     answers: [],
     selected: [],
@@ -26,6 +29,13 @@ const emptyMarker = {
     y: 0,
 };
 
+const emptyAnswerlessMarker: OptionalAnswersMarkerType = {
+    label: "",
+    selected: [],
+    x: 0,
+    y: 0,
+};
+
 describe("LabelImage", function () {
     let userEvent: UserEvent;
     beforeEach(() => {
@@ -118,7 +128,7 @@ describe("LabelImage", function () {
 
     describe("navigateToMarkerIndex", function () {
         it("should not navigate if all other markers were answered", function () {
-            const markers = [
+            const markers: ReadonlyArray<InteractiveMarkerType> = [
                 {
                     ...emptyMarker,
                     label: "this",
@@ -151,505 +161,509 @@ describe("LabelImage", function () {
 
             expect(
                 markers[
-                    // @ts-expect-error - TS2345 - Argument of type '({ label: string; answers: readonly []; selected: readonly []; x: 0; y: 0; } | { label: string; y: number; showCorrectness: string; answers: readonly []; selected: readonly []; x: 0; } | { label: string; x: number; showCorrectness: string; answers: readonly []; selected: readonly []; y: 0; })[]' is not assignable to parameter of type 'readonly InteractiveMarkerType[]'.
                     LabelImage.navigateToMarkerIndex({x: 0, y: 0}, markers, 0)
                 ].label,
             ).toEqual("this");
         });
 
-        describe("up direction", function () {
-            const navigateDirecton = {x: 0, y: -1} as const;
-
-            it("should navigate to marker above", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above");
-            });
-
-            it("should navigate to lowest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "lowest",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("lowest");
-            });
+        describe.each([
+            ["answerless", emptyAnswerlessMarker],
+            ["answerful", emptyMarker],
+        ])("given %s options", (_, marker) => {
+            describe("up direction", function () {
+                const navigateDirecton = {x: 0, y: -1} as const;
 
-            it("should navigate to the next above marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 1",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 2",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("right direction", function () {
-            const navigateDirecton = {x: 1, y: 0} as const;
-
-            it("should navigate to marker on the right", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right");
-            });
-
-            it("should navigate to leftmost marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "leftmost",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("leftmost");
-            });
-
-            it("should navigate to the next right marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 1",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 2",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("down direction", function () {
-            const navigateDirecton = {x: 0, y: 1} as const;
-
-            it("should navigate to marker below", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("below");
-            });
-
-            it("should navigate to highest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "highest",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("highest");
+                it("should navigate to marker above", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "below",
+                            y: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "above",
+                            y: -1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("above");
+                });
+
+                it("should navigate to lowest marker", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "lowest",
+                            y: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "below",
+                            y: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("lowest");
+                });
+
+                it("should navigate to the next above marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "above 1",
+                            y: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "above 2",
+                            y: -1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("above 1");
+                });
+
+                it("should navigate to the next coplanar marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 1",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 2",
+                            y: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("coplanar 1");
+                });
             });
 
-            it("should navigate to the next below marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below 1",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below 2",
-                        y: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("below 1");
-            });
+            describe("right direction", function () {
+                const navigateDirecton = {x: 1, y: 0} as const;
 
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
+                it("should navigate to marker on the right", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "left",
+                            x: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "right",
+                            x: 1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("right");
+                });
+
+                it("should navigate to leftmost marker", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "leftmost",
+                            x: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "left",
+                            x: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("leftmost");
+                });
+
+                it("should navigate to the next right marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "right 1",
+                            x: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "right 2",
+                            x: 1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("right 1");
+                });
+
+                it("should navigate to the next coplanar marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 1",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 2",
+                            x: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("coplanar 1");
+                });
             });
-        });
 
-        describe("left direction", function () {
-            const navigateDirecton = {x: -1, y: 0} as const;
-
-            it("should navigate to marker on the left", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("left");
-            });
+            describe("down direction", function () {
+                const navigateDirecton = {x: 0, y: 1} as const;
 
-            it("should navigate to rightmost marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "rightmost",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("rightmost");
+                it("should navigate to marker below", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "above",
+                            y: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "below",
+                            y: 1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("below");
+                });
+
+                it("should navigate to highest marker", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "highest",
+                            y: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "above",
+                            y: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("highest");
+                });
+
+                it("should navigate to the next below marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "below 1",
+                            y: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "below 2",
+                            y: 1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("below 1");
+                });
+
+                it("should navigate to the next coplanar marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 1",
+                            y: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 2",
+                            y: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("coplanar 1");
+                });
             });
 
-            it("should navigate to the next left marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left 1",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left 2",
-                        x: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("left 1");
-            });
+            describe("left direction", function () {
+                const navigateDirecton = {x: -1, y: 0} as const;
 
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
+                it("should navigate to marker on the left", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "right",
+                            x: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "left",
+                            x: -1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("left");
+                });
+
+                it("should navigate to rightmost marker", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "rightmost",
+                            x: 1,
+                        },
+                        {
+                            ...marker,
+                            label: "right",
+                            x: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("rightmost");
+                });
+
+                it("should navigate to the next left marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "left 1",
+                            x: -1,
+                        },
+                        {
+                            ...marker,
+                            label: "left 2",
+                            x: -1,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("left 1");
+                });
+
+                it("should navigate to the next coplanar marker in original order", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 1",
+                            x: 0,
+                        },
+                        {
+                            ...marker,
+                            label: "coplanar 2",
+                            x: 0,
+                        },
+                    ];
+
+                    expect(
+                        markers[
+                            LabelImage.navigateToMarkerIndex(
+                                navigateDirecton,
+                                markers,
+                                0,
+                            )
+                        ].label,
+                    ).toEqual("coplanar 1");
+                });
             });
         });
     });
@@ -769,10 +783,40 @@ describe("LabelImage", function () {
             // Assert
             expect(score).toHaveInvalidInput();
         });
+    });
 
-        it("can be answered correctly when correct option is picked for the marker", async () => {
+    const answerfulItem = generateTestPerseusItem({
+        question: shortTextQuestion,
+    });
+    const answerlessItem = splitPerseusItem(answerfulItem);
+
+    test("the answerless test data doesn't contain answers", () => {
+        // Arrange / Act / Assert
+        expect(
+            answerlessItem.question.widgets["label-image 1"].options.markers[0],
+        ).not.toEqual(
+            answerfulItem.question.widgets["label-image 1"].options.markers[0],
+        );
+    });
+
+    describe.each([
+        ["answerless", answerlessItem],
+        ["answerful", answerfulItem],
+    ])("given %s options", (_, {question}) => {
+        it("renders", async () => {
+            // Arrange / Act
+            renderQuestion(question);
+            // Assert
+            expect(
+                await screen.findByText(
+                    "Click each dot on the image to select an answer.",
+                ),
+            ).toBeInTheDocument();
+        });
+
+        it("can be answered", async () => {
             // Arrange
-            const {renderer} = renderQuestion(shortTextQuestion);
+            const {renderer} = renderQuestion(question);
 
             // Act
             const markerButton = await screen.findByLabelText(
@@ -783,18 +827,40 @@ describe("LabelImage", function () {
             const choice = screen.getByRole("option", {name: "SUVs"});
             await userEvent.click(choice);
 
-            const score = scorePerseusItemTesting(
-                shortTextQuestion,
-                renderer.getUserInputMap(),
+            const userInput = renderer.getUserInputMap();
+
+            // Assert
+            expect(userInput).toEqual({
+                "label-image 1": {
+                    markers: [
+                        {
+                            label: "The fourth unlabeled bar line.",
+                            selected: ["SUVs"],
+                        },
+                    ],
+                },
+            });
+        });
+
+        it("can give an invalid score", () => {
+            // Arrange
+            const {renderer} = renderQuestion(question);
+
+            // Act
+            const userInput = renderer.getUserInputMap();
+            const score = scorePerseusItem(
+                answerfulItem.question,
+                userInput,
+                "en",
             );
 
             // Assert
-            expect(score).toHaveBeenAnsweredCorrectly();
+            expect(score).toHaveInvalidInput();
         });
 
-        it("can be answered incorrectly when incorrect option picked for the marker", async () => {
+        it("can be answered correctly when correct option is picked for the marker", async () => {
             // Arrange
-            const {renderer} = renderQuestion(shortTextQuestion);
+            const {renderer} = renderQuestion(question);
 
             // Act
             const markerButton = await screen.findByLabelText(
@@ -802,30 +868,38 @@ describe("LabelImage", function () {
             );
             await userEvent.click(markerButton);
 
-            const choice = screen.getByRole("option", {name: "Trucks"});
+            const choice = screen.getByRole("option", {name: "SUVs"});
             await userEvent.click(choice);
 
             const score = scorePerseusItemTesting(
-                shortTextQuestion,
+                answerfulItem.question,
                 renderer.getUserInputMap(),
             );
 
             // Assert
-            expect(score).toHaveBeenAnsweredIncorrectly();
+            expect(score).toHaveBeenAnsweredCorrectly();
         });
-    });
 
-    describe("textWithoutAnswersQuestion", () => {
-        it("should render the widget without answers", async () => {
+        it("can be answered incorrectly when incorrect option picked for the marker", async () => {
             // Arrange
-            renderQuestion(textWithoutAnswersQuestion);
+            const {renderer} = renderQuestion(question);
 
-            // Act and Assert
+            // Act
             const markerButton = await screen.findByLabelText(
                 "The fourth unlabeled bar line.",
             );
-            // Confirms the widget renders and that marker buttons are present
             await userEvent.click(markerButton);
+
+            const choice = screen.getByRole("option", {name: "Trucks"});
+            await userEvent.click(choice);
+
+            const score = scorePerseusItemTesting(
+                answerfulItem.question,
+                renderer.getUserInputMap(),
+            );
+
+            // Assert
+            expect(score).toHaveBeenAnsweredIncorrectly();
         });
     });
 });
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 fc606511cde..524e1c8691c 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
@@ -106,68 +106,6 @@ export const shortTextQuestion: PerseusRenderer = {
     },
 };
 
-export const textWithoutAnswersQuestion: PerseusRenderer = {
-    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",
-            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: [],
-                        label: "The fourth unlabeled bar line.",
-                        x: 25,
-                        y: 17.7,
-                    },
-                    {
-                        answers: [],
-                        label: "The third unlabeled bar line.",
-                        x: 25,
-                        y: 35.3,
-                    },
-                    {
-                        answers: [],
-                        label: "The second unlabeled bar line.",
-                        x: 25,
-                        y: 53,
-                    },
-                    {
-                        answers: [],
-                        label: "The first unlabeled bar line.",
-                        x: 25,
-                        y: 70.3,
-                    },
-                ],
-                multipleAnswers: false,
-                hideChoicesFromInstructions: true,
-            },
-            version: {
-                major: 0,
-                minor: 0,
-            },
-        },
-    },
-};
-
 export const mathQuestion: PerseusRenderer = {
     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",
diff --git a/packages/perseus/src/widgets/label-image/label-image.tsx b/packages/perseus/src/widgets/label-image/label-image.tsx
index 8bc9511db5a..e8adf9b9c7a 100644
--- a/packages/perseus/src/widgets/label-image/label-image.tsx
+++ b/packages/perseus/src/widgets/label-image/label-image.tsx
@@ -28,14 +28,15 @@ import {HideAnswersToggle} from "./hide-answers-toggle";
 import Marker from "./marker";
 
 import type {DependencyProps} from "../../dependencies";
-import type {ChangeableProps} from "../../mixins/changeable";
-import type {APIOptions, Widget, WidgetExports} from "../../types";
+import type {Widget, WidgetExports, WidgetProps} from "../../types";
 import type {LabelImagePromptJSON} from "../../widget-ai-utils/label-image/label-image-ai-utils";
 import type {
     InteractiveMarkerType,
     PerseusLabelImageWidgetOptions,
     PerseusLabelImageUserInput,
+    LabelImagePublicWidgetOptions,
 } from "@khanacademy/perseus-core";
+import type {InteractiveMarkerScore} from "@khanacademy/perseus-score/src/widgets/label-image/score-label-image";
 import type {PropsFor} from "@khanacademy/wonder-blocks-core";
 import type {CSSProperties} from "aphrodite";
 
@@ -64,15 +65,22 @@ type Point = {
     y: number;
 };
 
-type LabelImageProps = ChangeableProps &
-    DependencyProps &
-    Omit<PerseusLabelImageWidgetOptions, "markers"> & {
-        apiOptions: APIOptions;
-        // The list of label markers on the question image.
-        markers: ReadonlyArray<InteractiveMarkerType>;
-        // Whether the question has been answered by the user.
-        questionCompleted: boolean;
+export type OptionalAnswersMarkerType = Omit<
+    InteractiveMarkerType,
+    "answers"
+> & {
+    answers?: string[];
+};
+
+type Options = Omit<PerseusLabelImageWidgetOptions, "markers"> & {
+    // The list of label markers on the question image.
+    markers: ReadonlyArray<OptionalAnswersMarkerType>;
+};
+
+type Props = WidgetProps<Options> &
+    DependencyProps & {
         // preferred placement for popover (preference, not MUST)
+        // TODO: this is sus, probably never passed in
         preferredPopoverDirection?: PreferredPopoverDirection;
     };
 
@@ -85,8 +93,14 @@ type LabelImageState = {
     hideAnswers: boolean;
 };
 
+function isAnswerful(
+    marker: OptionalAnswersMarkerType | InteractiveMarkerType,
+): marker is InteractiveMarkerType {
+    return marker.answers != null;
+}
+
 export class LabelImage
-    extends React.Component<LabelImageProps, LabelImageState>
+    extends React.Component<Props, LabelImageState>
     implements Widget
 {
     static contextType = PerseusI18nContext;
@@ -102,7 +116,7 @@ export class LabelImage
      * Implementation taken from: https://stackoverflow.com/a/2049593
      */
     static pointInTriangle(p: Point, a: Point, b: Point, c: Point): boolean {
-        const sign = (p1: Point, p2: Point, p3) =>
+        const sign = (p1: Point, p2: Point, p3: Point) =>
             (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
 
         const b1 = sign(p, a, b) < 0;
@@ -152,24 +166,26 @@ export class LabelImage
         const bl = {x: 20, y: 100} as const;
         const cp = {x: 50, y: 50} as const;
 
+        const sides = ["top", "right", "bottom", "left"] as const;
+
+        type Side = (typeof sides)[number];
+
         // The triangles representing the sides to test.
-        const triangles = {
+        const triangles: Record<Side, [Point, Point, Point]> = {
             top: [tl, tr, cp],
             right: [cp, tr, br],
             bottom: [bl, cp, br],
             left: [tl, cp, bl],
-        } as const;
+        };
 
         const p = {x, y} as const;
 
         // Test whether marker is positioned within one of the triangles
         // representing the sides.
-        for (const side of Object.keys(triangles)) {
+        for (const side of sides) {
             const corners = triangles[side];
 
-            // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
             if (LabelImage.pointInTriangle(p, ...corners)) {
-                // @ts-expect-error - TS2322 - Type 'string' is not assignable to type '"left" | "top" | "center" | "right" | "bottom"'.
                 return side;
             }
         }
@@ -186,7 +202,11 @@ export class LabelImage
      */
     static navigateToMarkerIndex(
         navigateDirection: Direction,
-        markers: LabelImageProps["markers"],
+        markers: ReadonlyArray<{
+            x: number;
+            y: number;
+            showCorrectness?: "correct" | "incorrect";
+        }>,
         thisIndex: number,
     ): number {
         const thisMarker = markers[thisIndex];
@@ -281,7 +301,7 @@ export class LabelImage
         return sortedMarkers.length > 0 ? sortedMarkers[0].index : thisIndex;
     }
 
-    constructor(props: LabelImageProps) {
+    constructor(props: Props) {
         super(props);
 
         this._markers = [];
@@ -314,39 +334,41 @@ export class LabelImage
         return _getPromptJSON(this.props, this.getUserInput());
     }
 
-    // TODO(LEMS-2544): Investigate impact on scoring
-    // Also consider how scoreMarker is being called as it seems to require the marker.answers property.
-    // Removed rubric parameter, but it gets a full widget options object from the renderer
     showRationalesForCurrentlySelectedChoices() {
         const {markers} = this.props;
         const {onChange} = this.props;
 
         const updatedMarkers = markers.map((marker) => {
-            const score = scoreLabelImageMarker(
-                marker.selected,
-                marker.answers,
-            );
+            if (isAnswerful(marker)) {
+                const score = scoreLabelImageMarker(
+                    marker.selected,
+                    marker.answers,
+                );
 
+                return {
+                    ...marker,
+                    // Reveal correctness state for markers with answers.
+                    showCorrectness: score.hasAnswers
+                        ? score.isCorrect
+                            ? "correct"
+                            : "incorrect"
+                        : undefined,
+                };
+            }
+            // If the marker doesn't have answers, retain its current state
+            // or set showCorrectness to undefined.
             return {
                 ...marker,
-                // Reveal correctness state for markers with answers.
-                showCorrectness: score.hasAnswers
-                    ? score.isCorrect
-                        ? "correct"
-                        : "incorrect"
-                    : undefined,
+                showCorrectness: undefined,
             };
         });
 
         // Update Perseus widget state with user selected answers without
         // triggering interaction events for listeners.
-        onChange({markers: updatedMarkers}, null, true);
+        onChange({markers: updatedMarkers}, undefined, true);
     }
 
-    handleMarkerChange(
-        index: number,
-        marker: LabelImageProps["markers"][number],
-    ) {
+    handleMarkerChange(index: number, marker: Props["markers"][number]) {
         const {markers, onChange} = this.props;
 
         // Replace marker with a changed version at the specified index.
@@ -398,24 +420,26 @@ export class LabelImage
         }
 
         // Only navigation in the "x" or "y" axis is supported, no diagonals.
-        const navigateDirection = {
+        const directions: Record<string, Direction> = {
             ArrowUp: {x: 0, y: -1},
             ArrowRight: {x: 1, y: 0},
             ArrowDown: {x: 0, y: 1},
             ArrowLeft: {x: -1, y: 0},
-        }[e.key];
+        };
 
-        if (!navigateDirection) {
+        // Return early if key is not an arrow key
+        if (!(e.key in directions)) {
             return;
         }
 
+        const navigateDirection = directions[e.key];
+
         e.preventDefault();
 
         // Focus on the closest marker along the direction of navigation.
         const marker =
             this._markers[
                 LabelImage.navigateToMarkerIndex(
-                    // @ts-expect-error - TS2345 - Argument of type '{ x: number; y: number; } | { x: number; y: number; } | { x: number; y: number; } | { x: number; y: number; }' is not assignable to parameter of type 'Direction'.
                     navigateDirection,
                     markers,
                     index,
@@ -442,7 +466,6 @@ export class LabelImage
             selected: selected.length ? selected : undefined,
         });
     }
-    // TODO(LEMS-2723): Investigate if possible to change this to not require answers
     renderMarkers(): ReadonlyArray<React.ReactNode> {
         const {markers, questionCompleted, preferredPopoverDirection} =
             this.props;
@@ -487,10 +510,15 @@ export class LabelImage
                 }[markerPosition];
             }
 
-            const score = scoreLabelImageMarker(
-                marker.selected,
-                marker.answers,
-            );
+            let score: InteractiveMarkerScore;
+            if (isAnswerful(marker)) {
+                score = scoreLabelImageMarker(marker.selected, marker.answers);
+            } else {
+                score = {
+                    hasAnswers: false,
+                    isCorrect: false,
+                };
+            }
             // Once the question is answered, show markers
             // with correct answers, otherwise passthrough
             // the correctness state.
@@ -564,7 +592,7 @@ export class LabelImage
                             >
                                 {({hovered, focused, pressed}) => (
                                     <Marker
-                                        {...marker}
+                                        label={marker.label}
                                         showCorrectness={showCorrectness}
                                         showSelected={opened}
                                         showPulsate={!markersInteracted}
@@ -574,9 +602,9 @@ export class LabelImage
                                         showAnswer={showAnswerChoice}
                                         answerSide={side}
                                         answerStyles={adjustPillDistance}
-                                        analytics={this.props.analytics}
                                         focused={focused || pressed}
                                         hovered={hovered}
+                                        selected={marker.selected}
                                     />
                                 )}
                             </Clickable>
@@ -761,6 +789,14 @@ const LabelImageWithDependencies = React.forwardRef<
     return <LabelImage ref={ref} analytics={deps.analytics} {...props} />;
 });
 
+({}) as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
+
+({}) as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
+
 export default {
     name: "label-image",
     displayName: "Label Image",
diff --git a/packages/perseus/src/widgets/label-image/marker.tsx b/packages/perseus/src/widgets/label-image/marker.tsx
index e2f5e213691..b3bf4541ba7 100644
--- a/packages/perseus/src/widgets/label-image/marker.tsx
+++ b/packages/perseus/src/widgets/label-image/marker.tsx
@@ -17,13 +17,12 @@ import {iconCheck, iconChevronDown, iconMinus} from "../../icon-paths";
 import {AnswerPill} from "./answer-pill";
 
 import type {IconType} from "../../components/icon";
-import type {
-    AnalyticsEventHandlerFn,
-    InteractiveMarkerType,
-} from "@khanacademy/perseus-core";
 import type {CSSProperties} from "aphrodite";
 
-type Props = InteractiveMarkerType & {
+type Props = {
+    selected?: string[];
+    showCorrectness?: "correct" | "incorrect";
+    label: string;
     // Whether this marker has been selected by user.
     showSelected: boolean;
     // Whether this marker should pulsate to draw user attention.
@@ -31,9 +30,6 @@ type Props = InteractiveMarkerType & {
     answerSide: "top" | "bottom" | "left" | "right";
     answerStyles?: CSSProperties;
     showAnswer?: boolean;
-    analytics?: {
-        onAnalyticsEvent: AnalyticsEventHandlerFn;
-    };
     focused: boolean;
     hovered: boolean;
 };


Reviews:
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T16:20:16Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T16:58:49Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates the Label Image widget to support cases where marker data does not include `answers`, refactors related types and logic, and adds tests and a Storybook story for “answerless” mode.

- Allow markers without answers by adjusting prop and type definitions
- Refactor `LabelImage` component to handle answerless data and clean up navigation logic
- Update tests and stories to generate and verify both answerful and answerless scenarios

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                                                   | Description                                                      |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| packages/perseus/src/widgets/label-image/marker.tsx                                    | Updated `Props` to accept optional answers and removed analytics |
| packages/perseus/src/widgets/label-image/label-image.tsx                               | Refactored types, handle `answerless` markers, cleaned up TS errors, and improved navigation logic |
| packages/perseus/src/widgets/label-image/__tests__/label-image.testdata.ts             | Removed legacy answerless testdata and rely on splitPerseusItem  |
| packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts                 | Switched to `generateTestPerseusItem`/`splitPerseusItem`, parameterized tests for answerful vs answerless |
| packages/perseus/src/widgets/label-image/__stories__/label-image.stories.tsx           | Added `LabelWidgetAnswerless` story                              |
| packages/perseus/src/types.ts                                                          | Exposed marker types in `ChangeHandler`                          |
| packages/perseus-core/src/widgets/label-image/label-image-util.ts                      | Exported public widget options and marker data types             |
| packages/perseus-core/src/index.ts                                                     | Re-exported new label-image types                                |
| .changeset/green-yaks-scream.md                                                         | Bumped versions and summarized changes                           |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts:791**
* [nitpick] The test description `can give invalid score` is generic; consider renaming to something like `should report unanswered markers as invalid input` for clarity across both scenarios.
```
it("can give invalid score", () => {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-16T18:07:26Z
user: login: benchristel
body: I think the types and code could be simplified if we had a slightly different implementation of `getLabelImageMarkerPublicData`.

Instead of completely removing `answers` from the markers when getting the answerless data, we can replace `answers` with an empty array:

```
export type LabelImageMarkerPublicData = PerseusLabelImageMarker;

function getLabelImageMarkerPublicData(
    marker: PerseusLabelImageMarker,
): LabelImageMarkerPublicData {
    return {
        ...marker,
        answers: [],
    }
}
```

If we do that, we can make a lot of simplifications:

- The `LabelImagePublicWidgetOptions` type can just be an alias for `PerseusLabelImageWidgetOptions`.
- We don't need to default `answers` to `[]` in `renderMarkers` and `showRationalesForCurrentlySelectedChoices`.
- Other simplifications noted inline.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T20:12:14Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:33:12Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:56:33Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:58:49Z
user: login: handeyeco
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T18:48:38Z
user: login: benchristel
body: LGTM! I'll leave it up to you what to do with the types.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T22:16:58Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T20:38:18Z
user: login: benchristel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T21:50:12Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T22:10:36Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T19:07:56Z
user: login: Myranae
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T19:18:02Z
user: login: benchristel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T19:26:59Z

Review comments:
diff_hunk: @@ -17,23 +17,19 @@ import {iconCheck, iconChevronDown, iconMinus} from "../../icon-paths";
 import {AnswerPill} from "./answer-pill";
 
 import type {IconType} from "../../components/icon";
-import type {
-    AnalyticsEventHandlerFn,
-    InteractiveMarkerType,
-} from "@khanacademy/perseus-core";
 import type {CSSProperties} from "aphrodite";
 
-type Props = InteractiveMarkerType & {
+type Props = {
+    selected?: string[];
+    showCorrectness?: "correct" | "incorrect";
+    label: string;
path: packages/perseus/src/widgets/label-image/marker.tsx
user: login: Myranae
body: Left out `answers`, `x`, and `y`. Focus is already included down below. X and y don't seem to be used, but if they are added back as Props, there's no error. Since they don't seem to be used by Marker though, I just left them out.
created_at: 2025-05-16T16:20:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,15 +64,22 @@ type Point = {
     y: number;
 };
 
-type LabelImageProps = ChangeableProps &
-    DependencyProps &
-    Omit<PerseusLabelImageWidgetOptions, "markers"> & {
-        apiOptions: APIOptions;
-        // The list of label markers on the question image.
-        markers: ReadonlyArray<InteractiveMarkerType>;
-        // Whether the question has been answered by the user.
-        questionCompleted: boolean;
+export type OptionalAnswersMarkerType = Omit<
+    InteractiveMarkerType,
+    "answers"
+> & {
+    answers?: string[];
+};
+
+type Options = Omit<PerseusLabelImageWidgetOptions, "markers"> & {
+    // The list of label markers on the question image.
+    markers: ReadonlyArray<OptionalAnswersMarkerType>;
+};
+
+type Props = WidgetProps<Options> &
+    DependencyProps & {
         // preferred placement for popover (preference, not MUST)
+        // TODO: this is sus, probably never passed in
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Myranae
body: Did a short search in Webapp and found this included in a label image schema in the widget.go file. Curious as it's not in `PerseusLabelImageWidgetOptions`. It's available in the label image editor as something to set, but it doesn't actually let you set it. 😂 Maybe this is why. Going to make a ticket and put my findings there. Not required for SSS though.
created_at: 2025-05-16T16:58:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,15 +64,22 @@ type Point = {
     y: number;
 };
 
-type LabelImageProps = ChangeableProps &
-    DependencyProps &
-    Omit<PerseusLabelImageWidgetOptions, "markers"> & {
-        apiOptions: APIOptions;
-        // The list of label markers on the question image.
-        markers: ReadonlyArray<InteractiveMarkerType>;
-        // Whether the question has been answered by the user.
-        questionCompleted: boolean;
+export type OptionalAnswersMarkerType = Omit<
+    InteractiveMarkerType,
+    "answers"
+> & {
+    answers?: string[];
+};
+
+type Options = Omit<PerseusLabelImageWidgetOptions, "markers"> & {
+    // The list of label markers on the question image.
+    markers: ReadonlyArray<OptionalAnswersMarkerType>;
+};
+
+type Props = WidgetProps<Options> &
+    DependencyProps & {
         // preferred placement for popover (preference, not MUST)
+        // TODO: this is sus, probably never passed in
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Copilot
body: [nitpick] Consider removing or resolving this TODO now that `preferredPopoverDirection` is stabilized or add a comment explaining its future use.
```suggestion
        // Preferred placement for the popover. This is a preference, not a strict requirement.
        // Used to determine the initial direction of the popover when displayed.
```
created_at: 2025-05-16T18:07:25Z
author_association: NONE
diff_hunk: @@ -761,6 +770,14 @@ const LabelImageWithDependencies = React.forwardRef<
     return <LabelImage ref={ref} analytics={deps.analytics} {...props} />;
 });
 
+0 as any as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
+
+0 as any as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Copilot
body: [nitpick] The `0 as any as WidgetProps...` lines are a non-intuitive type hack; consider documenting their purpose or replacing with a clearer type assertion to improve readability.
```suggestion
const perseusLabelImageWidgetProps: WidgetProps<PerseusLabelImageWidgetOptions> = {} as WidgetProps<PerseusLabelImageWidgetOptions>;
perseusLabelImageWidgetProps satisfies PropsFor<typeof LabelImage>;

const labelImagePublicWidgetProps: WidgetProps<LabelImagePublicWidgetOptions> = {} as WidgetProps<LabelImagePublicWidgetOptions>;
labelImagePublicWidgetProps satisfies PropsFor<typeof LabelImage>;
```
created_at: 2025-05-16T18:07:26Z
author_association: NONE
diff_hunk: @@ -150,6 +152,8 @@ export type ChangeHandler = (
         numLinePosition?: number;
         // widgets/number-line.ts
         rel?: Relationship;
+        // widgets/label-image.tsx
+        markers?: Array<LabelImageMarkerPublicData | PerseusLabelImageMarker>;
path: packages/perseus/src/types.ts
user: login: benchristel
body: If we default `answers` to `[]` in `getLabelImageMarkerPublicData`, this can become

```suggestion
        markers?: LabelImageMarkerPublicData[];
```
created_at: 2025-05-16T19:33:20Z
author_association: MEMBER
diff_hunk: @@ -64,15 +64,22 @@ type Point = {
     y: number;
 };
 
-type LabelImageProps = ChangeableProps &
-    DependencyProps &
-    Omit<PerseusLabelImageWidgetOptions, "markers"> & {
-        apiOptions: APIOptions;
-        // The list of label markers on the question image.
-        markers: ReadonlyArray<InteractiveMarkerType>;
-        // Whether the question has been answered by the user.
-        questionCompleted: boolean;
+export type OptionalAnswersMarkerType = Omit<
+    InteractiveMarkerType,
+    "answers"
+> & {
+    answers?: string[];
+};
+
+type Options = Omit<PerseusLabelImageWidgetOptions, "markers"> & {
+    // The list of label markers on the question image.
+    markers: ReadonlyArray<OptionalAnswersMarkerType>;
+};
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: The `Options` type can go away if we default `answers` — replaced by `PerseusLabelImageWidgetOptions`. Same with `OptionalAnswersMarkerType` — it just becomes `InteractiveMarkerType`.
created_at: 2025-05-16T19:37:27Z
author_association: MEMBER
diff_hunk: @@ -314,17 +328,14 @@ export class LabelImage
         return _getPromptJSON(this.props, this.getUserInput());
     }
 
-    // TODO(LEMS-2544): Investigate impact on scoring
-    // Also consider how scoreMarker is being called as it seems to require the marker.answers property.
-    // Removed rubric parameter, but it gets a full widget options object from the renderer
     showRationalesForCurrentlySelectedChoices() {
         const {markers} = this.props;
         const {onChange} = this.props;
 
         const updatedMarkers = markers.map((marker) => {
             const score = scoreLabelImageMarker(
                 marker.selected,
-                marker.answers,
+                marker.answers ?? [],
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: If we default `answers` when getting answerless widget options, we don't need this default:

```suggestion
                marker.answers,
```
created_at: 2025-05-16T19:43:24Z
author_association: MEMBER
diff_hunk: @@ -489,7 +498,7 @@ export class LabelImage
 
             const score = scoreLabelImageMarker(
                 marker.selected,
-                marker.answers,
+                marker.answers ?? [],
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: Same here:

```suggestion
                marker.answers,
```
created_at: 2025-05-16T19:45:10Z
author_association: MEMBER
diff_hunk: @@ -574,9 +583,9 @@ export class LabelImage
                                         showAnswer={showAnswerChoice}
                                         answerSide={side}
                                         answerStyles={adjustPillDistance}
-                                        analytics={this.props.analytics}
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: Looks like `Marker` doesn't refer to `analytics` at all. 👍 
created_at: 2025-05-16T19:46:34Z
author_association: MEMBER
diff_hunk: @@ -761,6 +770,14 @@ const LabelImageWithDependencies = React.forwardRef<
     return <LabelImage ref={ref} analytics={deps.analytics} {...props} />;
 });
 
+0 as any as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
+
+0 as any as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: Copilot's rewrite is super verbose and not any clearer IMO. It also creates constants with useful-looking names but unusable values. 👎

However, I like the suggestion to use `{}` instead of `0` as the dummy value — assuming it does in fact let us avoid the intermediate cast to `any`.

```suggestion
{} as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
    typeof LabelImage
>;

{} as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
    typeof LabelImage
>;
```
created_at: 2025-05-16T19:49:20Z
author_association: MEMBER
diff_hunk: @@ -17,23 +17,19 @@ import {iconCheck, iconChevronDown, iconMinus} from "../../icon-paths";
 import {AnswerPill} from "./answer-pill";
 
 import type {IconType} from "../../components/icon";
-import type {
-    AnalyticsEventHandlerFn,
-    InteractiveMarkerType,
-} from "@khanacademy/perseus-core";
 import type {CSSProperties} from "aphrodite";
 
-type Props = InteractiveMarkerType & {
+type Props = {
+    selected?: string[];
+    showCorrectness?: "correct" | "incorrect";
+    label: string;
path: packages/perseus/src/widgets/label-image/marker.tsx
user: login: benchristel
body: Yep, looks like `x` and `y` are not needed insider Marker, because `LabelImage` positions an ancestor element of the marker using `x` and `y`.

> if they are added back as Props, there's no error.

That's kind of surprising. `LabelImage` isn't passing x and y to the `Marker`, so I'd expect there to be an error if they're required as props.

FWIW I tried adding `x: number` here and got an error (as I expected) because LabelImage isn't passing it.
created_at: 2025-05-16T19:54:59Z
author_association: MEMBER
diff_hunk: @@ -152,24 +159,31 @@ export class LabelImage
         const bl = {x: 20, y: 100} as const;
         const cp = {x: 50, y: 50} as const;
 
+        type Side = "top" | "right" | "bottom" | "left";
+
         // The triangles representing the sides to test.
-        const triangles = {
+        const triangles: Record<Side, [Point, Point, Point]> = {
             top: [tl, tr, cp],
             right: [cp, tr, br],
             bottom: [bl, cp, br],
             left: [tl, cp, bl],
-        } as const;
+        };
 
         const p = {x, y} as const;
 
         // Test whether marker is positioned within one of the triangles
         // representing the sides.
-        for (const side of Object.keys(triangles)) {
+        for (const side of Object.keys(triangles) as Side[]) {
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: Minor thing, but as long as we're fixing the types here I figure I'll mention it: I wonder if we could replace this `Object.keys` iteration with something that doesn't require the unsafe `as Side[]` cast. Maybe:

```ts
const sides = ["top", "right", "bottom", "left"] as const;

type Side = typeof sides[number];

const triangles: Record<Side, [Point, Point, Point]> = {
    top: [tl, tr, cp],
    right: [cp, tr, br],
    bottom: [bl, cp, br],
    left: [tl, cp, bl],
};

for (const side of sides) {
    // ...
}
```
created_at: 2025-05-16T20:10:52Z
author_association: MEMBER
diff_hunk: @@ -152,24 +159,31 @@ export class LabelImage
         const bl = {x: 20, y: 100} as const;
         const cp = {x: 50, y: 50} as const;
 
+        type Side = "top" | "right" | "bottom" | "left";
+
         // The triangles representing the sides to test.
-        const triangles = {
+        const triangles: Record<Side, [Point, Point, Point]> = {
             top: [tl, tr, cp],
             right: [cp, tr, br],
             bottom: [bl, cp, br],
             left: [tl, cp, bl],
-        } as const;
+        };
 
         const p = {x, y} as const;
 
         // Test whether marker is positioned within one of the triangles
         // representing the sides.
-        for (const side of Object.keys(triangles)) {
+        for (const side of Object.keys(triangles) as Side[]) {
             const corners = triangles[side];
 
-            // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
-            if (LabelImage.pointInTriangle(p, ...corners)) {
-                // @ts-expect-error - TS2322 - Type 'string' is not assignable to type '"left" | "top" | "center" | "right" | "bottom"'.
+            if (
+                LabelImage.pointInTriangle(
+                    p,
+                    corners[0],
+                    corners[1],
+                    corners[2],
+                )
+            ) {
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: With your other type changes (or the ones I proposed above), the spread syntax should work now:

```suggestion
            if (LabelImage.pointInTriangle(p, ...corners)) {
```
created_at: 2025-05-16T20:12:07Z
author_association: MEMBER
diff_hunk: @@ -761,6 +770,14 @@ const LabelImageWithDependencies = React.forwardRef<
     return <LabelImage ref={ref} analytics={deps.analytics} {...props} />;
 });
 
+0 as any as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
+
+0 as any as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Myranae
body: Hmm that seems to give an error saying `WidgetProps` is being used as a value. Cursor gave a suggestion for how to fix it (seems unnecessarily complicated), but I think the original way you had is probably fine.
```
// This will cause a compile error if the types are not compatible
type _Assert<T extends true> = T;
type _ = _Assert<WidgetProps<PerseusLabelImageWidgetOptions> extends PropsFor<typeof LabelImage> ? true : false>;
```
created_at: 2025-05-19T15:33:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,24 +159,31 @@ export class LabelImage
         const bl = {x: 20, y: 100} as const;
         const cp = {x: 50, y: 50} as const;
 
+        type Side = "top" | "right" | "bottom" | "left";
+
         // The triangles representing the sides to test.
-        const triangles = {
+        const triangles: Record<Side, [Point, Point, Point]> = {
             top: [tl, tr, cp],
             right: [cp, tr, br],
             bottom: [bl, cp, br],
             left: [tl, cp, bl],
-        } as const;
+        };
 
         const p = {x, y} as const;
 
         // Test whether marker is positioned within one of the triangles
         // representing the sides.
-        for (const side of Object.keys(triangles)) {
+        for (const side of Object.keys(triangles) as Side[]) {
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Myranae
body: Updated! Thanks. I dislike casting, so definitely like this more.
created_at: 2025-05-19T15:56:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,24 +159,31 @@ export class LabelImage
         const bl = {x: 20, y: 100} as const;
         const cp = {x: 50, y: 50} as const;
 
+        type Side = "top" | "right" | "bottom" | "left";
+
         // The triangles representing the sides to test.
-        const triangles = {
+        const triangles: Record<Side, [Point, Point, Point]> = {
             top: [tl, tr, cp],
             right: [cp, tr, br],
             bottom: [bl, cp, br],
             left: [tl, cp, bl],
-        } as const;
+        };
 
         const p = {x, y} as const;
 
         // Test whether marker is positioned within one of the triangles
         // representing the sides.
-        for (const side of Object.keys(triangles)) {
+        for (const side of Object.keys(triangles) as Side[]) {
             const corners = triangles[side];
 
-            // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
-            if (LabelImage.pointInTriangle(p, ...corners)) {
-                // @ts-expect-error - TS2322 - Type 'string' is not assignable to type '"left" | "top" | "center" | "right" | "bottom"'.
+            if (
+                LabelImage.pointInTriangle(
+                    p,
+                    corners[0],
+                    corners[1],
+                    corners[2],
+                )
+            ) {
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Myranae
body: Updated!
created_at: 2025-05-19T15:58:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,7 +493,7 @@ export class LabelImage
 
             const score = scoreLabelImageMarker(
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: handeyeco
body: I'm just going to start a thread here so we can have a conversation in one place (since GH doesn't allow for comments the other way we were talking).

> @benchristel @handeyeco Thanks for your input! I do feel like keeping answers with an empty array is misleading and would prefer to be in alignment with how the majority of widgets handle answers.
>
> Is there a reason to change to the empty array other than to simplify things? Could removing the answers field cause any issues or break anything down the line? If we decide to move forward with removing the answers field, do either of you have any suggestions for changes or feedback otherwise? @handeyeco, do you see someplace where we need to "not allow the calling of post-scoring logic with pre-scoring data" in this current PR?

---

> @handeyeco, do you see someplace where we need to "not allow the calling of post-scoring logic with pre-scoring data" in this current PR?

So my point is that it doesn't make sense calling some of this logic with answerless data. For instance it never makes sense to call any `score` function (like `scoreLabelImageMarker`) with answerless data; it's just a waste of time. It never makes sense to call `showRationalesForCurrentlySelectedChoices` with answerless data. There are no rationales in answerless data.

We could have a type predicate:

``` TS
function isAnswerful(options: LabelImageMarkerPublicData | PerseusLabelImageMarker)
  : options is PerseusLabelImageMarker {
    return options.answers != null;
  }
```

Then rather than calling `showRationalesForCurrentlySelectedChoices` we could skip it or throw if it's called before we have the answerful data:

``` TS
showRationalesForCurrentlySelectedChoices() {
  const {markers, onChange} = this.props;

  if (!isAnswerful(markers[0])) {
    throw new Error("showRationalesForCurrentlySelectedChoices called without answerful data!");
  }
```

Ideally the way we would do this would narrow the type, so we know that after the predicate we have `PerseusLabelImageMarker` and not `LabelImageMarkerPublicData`; there would be no need for `marker.answers ?? []`.

Admittedly this only makes the code more complicated, but it's much more explicit with our expectations and helps us uncover issues when we're expecting answerful data but not receiving it (rather than silently failing).
created_at: 2025-05-19T18:48:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,505 +161,509 @@ describe("LabelImage", function () {
 
             expect(
                 markers[
-                    // @ts-expect-error - TS2345 - Argument of type '({ label: string; answers: readonly []; selected: readonly []; x: 0; y: 0; } | { label: string; y: number; showCorrectness: string; answers: readonly []; selected: readonly []; x: 0; } | { label: string; x: number; showCorrectness: string; answers: readonly []; selected: readonly []; y: 0; })[]' is not assignable to parameter of type 'readonly InteractiveMarkerType[]'.
                     LabelImage.navigateToMarkerIndex({x: 0, y: 0}, markers, 0)
                 ].label,
             ).toEqual("this");
         });
 
-        describe("up direction", function () {
-            const navigateDirecton = {x: 0, y: -1} as const;
-
-            it("should navigate to marker above", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above");
-            });
-
-            it("should navigate to lowest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "lowest",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("lowest");
-            });
+        describe.each([
+            ["answerless", emptyAnswerlessMarker],
+            ["answerful", emptyMarker],
+        ])("given %s options", (_, marker) => {
+            describe("up direction", function () {
+                const navigateDirecton = {x: 0, y: -1} as const;
 
-            it("should navigate to the next above marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 1",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 2",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("right direction", function () {
-            const navigateDirecton = {x: 1, y: 0} as const;
-
-            it("should navigate to marker on the right", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right");
-            });
-
-            it("should navigate to leftmost marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "leftmost",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("leftmost");
-            });
-
-            it("should navigate to the next right marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 1",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 2",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("down direction", function () {
-            const navigateDirecton = {x: 0, y: 1} as const;
-
-            it("should navigate to marker below", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("below");
-            });
-
-            it("should navigate to highest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "highest",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("highest");
+                it("should navigate to marker above", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
path: packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
user: login: benchristel
body: Because we're merging the crucial fields (`label` and `y`) into the marker, these tests won't fail if answerless markers don't contain these fields. I don't think this is a huge problem, but it does make me wonder what bugs these tests could prevent.
created_at: 2025-05-19T22:02:38Z
author_association: MEMBER
diff_hunk: @@ -761,6 +770,14 @@ const LabelImageWithDependencies = React.forwardRef<
     return <LabelImage ref={ref} analytics={deps.analytics} {...props} />;
 });
 
+0 as any as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
+
+0 as any as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: benchristel
body: Oh, it might be because curly braces parse as a code block when a statement is expected. This works:

```ts
({}) as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
    typeof LabelImage
>;

({}) as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
    typeof LabelImage
>;
```

The parentheses force what's inside to be parsed as an expression rather than a statement (since parens can only be expressions, not statements).

created_at: 2025-05-19T22:14:45Z
author_association: MEMBER
diff_hunk: @@ -151,505 +161,509 @@ describe("LabelImage", function () {
 
             expect(
                 markers[
-                    // @ts-expect-error - TS2345 - Argument of type '({ label: string; answers: readonly []; selected: readonly []; x: 0; y: 0; } | { label: string; y: number; showCorrectness: string; answers: readonly []; selected: readonly []; x: 0; } | { label: string; x: number; showCorrectness: string; answers: readonly []; selected: readonly []; y: 0; })[]' is not assignable to parameter of type 'readonly InteractiveMarkerType[]'.
                     LabelImage.navigateToMarkerIndex({x: 0, y: 0}, markers, 0)
                 ].label,
             ).toEqual("this");
         });
 
-        describe("up direction", function () {
-            const navigateDirecton = {x: 0, y: -1} as const;
-
-            it("should navigate to marker above", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above");
-            });
-
-            it("should navigate to lowest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "lowest",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("lowest");
-            });
+        describe.each([
+            ["answerless", emptyAnswerlessMarker],
+            ["answerful", emptyMarker],
+        ])("given %s options", (_, marker) => {
+            describe("up direction", function () {
+                const navigateDirecton = {x: 0, y: -1} as const;
 
-            it("should navigate to the next above marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 1",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 2",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("right direction", function () {
-            const navigateDirecton = {x: 1, y: 0} as const;
-
-            it("should navigate to marker on the right", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right");
-            });
-
-            it("should navigate to leftmost marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "leftmost",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("leftmost");
-            });
-
-            it("should navigate to the next right marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 1",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 2",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("down direction", function () {
-            const navigateDirecton = {x: 0, y: 1} as const;
-
-            it("should navigate to marker below", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("below");
-            });
-
-            it("should navigate to highest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "highest",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("highest");
+                it("should navigate to marker above", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
path: packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
user: login: Myranae
body: So right now we have this test:
```
    test("the answerless test data doesn't contain answers", () => {
        // Arrange / Act / Assert
        expect(
            answerlessItem.question.widgets["label-image 1"].options.markers[0],
        ).not.toEqual(
            answerfulItem.question.widgets["label-image 1"].options.markers[0],
        );
    });
```

But what if we changed it to this test to more confirm it has what we expect. Would that help check what you're looking to check?
```
    test("the answerless test data is in the right shape", () => {
        // Arrange / Act / Assert
        const answerfulMarker = answerfulItem.question.widgets["label-image 1"].options.markers[0];

        expect(
            answerlessItem.question.widgets["label-image 1"].options.markers[0],
        ).toEqual(
            {label: answerfulMarker.label, x: answerfulMarker.x, y: answerfulMarker.y},
        );
    });
```
created_at: 2025-05-20T20:38:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,505 +161,509 @@ describe("LabelImage", function () {
 
             expect(
                 markers[
-                    // @ts-expect-error - TS2345 - Argument of type '({ label: string; answers: readonly []; selected: readonly []; x: 0; y: 0; } | { label: string; y: number; showCorrectness: string; answers: readonly []; selected: readonly []; x: 0; } | { label: string; x: number; showCorrectness: string; answers: readonly []; selected: readonly []; y: 0; })[]' is not assignable to parameter of type 'readonly InteractiveMarkerType[]'.
                     LabelImage.navigateToMarkerIndex({x: 0, y: 0}, markers, 0)
                 ].label,
             ).toEqual("this");
         });
 
-        describe("up direction", function () {
-            const navigateDirecton = {x: 0, y: -1} as const;
-
-            it("should navigate to marker above", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above");
-            });
-
-            it("should navigate to lowest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "lowest",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("lowest");
-            });
+        describe.each([
+            ["answerless", emptyAnswerlessMarker],
+            ["answerful", emptyMarker],
+        ])("given %s options", (_, marker) => {
+            describe("up direction", function () {
+                const navigateDirecton = {x: 0, y: -1} as const;
 
-            it("should navigate to the next above marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 1",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 2",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("right direction", function () {
-            const navigateDirecton = {x: 1, y: 0} as const;
-
-            it("should navigate to marker on the right", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right");
-            });
-
-            it("should navigate to leftmost marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "leftmost",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("leftmost");
-            });
-
-            it("should navigate to the next right marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 1",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 2",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("down direction", function () {
-            const navigateDirecton = {x: 0, y: 1} as const;
-
-            it("should navigate to marker below", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("below");
-            });
-
-            it("should navigate to highest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "highest",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("highest");
+                it("should navigate to marker above", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
path: packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
user: login: benchristel
body: Maybe? It's kind of indirect, though. It's hard to see the relationship between the two tests, and how one is supporting the other. In general, I would lean towards keeping tests very simple, because we can't write tests for our tests without getting into an infinite regress.

What we actually want to check here is that `navigateToMarkerIndex` doesn't interact with `answers`. The type system can do that for us, and do it more robustly than tests can. While tests can only demonstrate that the `answers` are not used in a specific set of scenarios, types can prove they're not used in _all_ scenarios.

What if we changed the type of `navigateToMarkerIndex` to:

```ts
static navigateToMarkerIndex(
    navigateDirection: Direction,
    markers: ReadonlyArray<{
        x: number,
        y: number,
        showCorrectness?: "correct" | "incorrect",
    }>,
    thisIndex: number,
): number {
  // ...
```

Then the type system will prove that `navigateToMarkerIndex` doesn't use `answers`, and we don't have to test both the answerful and answerless cases.
created_at: 2025-05-20T21:50:12Z
author_association: MEMBER
diff_hunk: @@ -489,7 +493,7 @@ export class LabelImage
 
             const score = scoreLabelImageMarker(
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Myranae
body: Okay, added the type predicate and some logic gating. I think I prefer just skipping the logic if it's called before answers are available, but looking at this again, maybe the Error throwing is better. It just stops the process from continuing, which is basically the same as skipping over it, but also lets us know something happened that shouldn't have. Maybe take a look at what I added, and if you think the error throwing is still better, I'll switch it.
created_at: 2025-05-20T22:10:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -761,6 +770,14 @@ const LabelImageWithDependencies = React.forwardRef<
     return <LabelImage ref={ref} analytics={deps.analytics} {...props} />;
 });
 
+0 as any as WidgetProps<PerseusLabelImageWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
+
+0 as any as WidgetProps<LabelImagePublicWidgetOptions> satisfies PropsFor<
+    typeof LabelImage
+>;
path: packages/perseus/src/widgets/label-image/label-image.tsx
user: login: Myranae
body: Updated this!
created_at: 2025-05-21T19:07:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,505 +161,509 @@ describe("LabelImage", function () {
 
             expect(
                 markers[
-                    // @ts-expect-error - TS2345 - Argument of type '({ label: string; answers: readonly []; selected: readonly []; x: 0; y: 0; } | { label: string; y: number; showCorrectness: string; answers: readonly []; selected: readonly []; x: 0; } | { label: string; x: number; showCorrectness: string; answers: readonly []; selected: readonly []; y: 0; })[]' is not assignable to parameter of type 'readonly InteractiveMarkerType[]'.
                     LabelImage.navigateToMarkerIndex({x: 0, y: 0}, markers, 0)
                 ].label,
             ).toEqual("this");
         });
 
-        describe("up direction", function () {
-            const navigateDirecton = {x: 0, y: -1} as const;
-
-            it("should navigate to marker above", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above");
-            });
-
-            it("should navigate to lowest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "lowest",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("lowest");
-            });
+        describe.each([
+            ["answerless", emptyAnswerlessMarker],
+            ["answerful", emptyMarker],
+        ])("given %s options", (_, marker) => {
+            describe("up direction", function () {
+                const navigateDirecton = {x: 0, y: -1} as const;
 
-            it("should navigate to the next above marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 1",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 2",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("right direction", function () {
-            const navigateDirecton = {x: 1, y: 0} as const;
-
-            it("should navigate to marker on the right", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right");
-            });
-
-            it("should navigate to leftmost marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "leftmost",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("leftmost");
-            });
-
-            it("should navigate to the next right marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 1",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 2",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("down direction", function () {
-            const navigateDirecton = {x: 0, y: 1} as const;
-
-            it("should navigate to marker below", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("below");
-            });
-
-            it("should navigate to highest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "highest",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("highest");
+                it("should navigate to marker above", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
path: packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
user: login: Myranae
body: So the type of `markers` here shows what info is necessary for the method to work, but if there is additional information in `markers` (like `answers`), then it will still be able to function correctly, right? I updated the type, so I'll remove the second example test we were considering since this should cover what we are hoping to check.
created_at: 2025-05-21T19:18:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,505 +161,509 @@ describe("LabelImage", function () {
 
             expect(
                 markers[
-                    // @ts-expect-error - TS2345 - Argument of type '({ label: string; answers: readonly []; selected: readonly []; x: 0; y: 0; } | { label: string; y: number; showCorrectness: string; answers: readonly []; selected: readonly []; x: 0; } | { label: string; x: number; showCorrectness: string; answers: readonly []; selected: readonly []; y: 0; })[]' is not assignable to parameter of type 'readonly InteractiveMarkerType[]'.
                     LabelImage.navigateToMarkerIndex({x: 0, y: 0}, markers, 0)
                 ].label,
             ).toEqual("this");
         });
 
-        describe("up direction", function () {
-            const navigateDirecton = {x: 0, y: -1} as const;
-
-            it("should navigate to marker above", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above");
-            });
-
-            it("should navigate to lowest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "lowest",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("lowest");
-            });
+        describe.each([
+            ["answerless", emptyAnswerlessMarker],
+            ["answerful", emptyMarker],
+        ])("given %s options", (_, marker) => {
+            describe("up direction", function () {
+                const navigateDirecton = {x: 0, y: -1} as const;
 
-            it("should navigate to the next above marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 1",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above 2",
-                        y: -1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("above 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("right direction", function () {
-            const navigateDirecton = {x: 1, y: 0} as const;
-
-            it("should navigate to marker on the right", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right");
-            });
-
-            it("should navigate to leftmost marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "leftmost",
-                        x: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "left",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("leftmost");
-            });
-
-            it("should navigate to the next right marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 1",
-                        x: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "right 2",
-                        x: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("right 1");
-            });
-
-            it("should navigate to the next coplanar marker in original order", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 1",
-                        x: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "coplanar 2",
-                        x: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("coplanar 1");
-            });
-        });
-
-        describe("down direction", function () {
-            const navigateDirecton = {x: 0, y: 1} as const;
-
-            it("should navigate to marker below", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 0,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "below",
-                        y: 1,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("below");
-            });
-
-            it("should navigate to highest marker", function () {
-                const markers = [
-                    {
-                        ...emptyMarker,
-                        label: "this",
-                        y: 1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "highest",
-                        y: -1,
-                    },
-                    {
-                        ...emptyMarker,
-                        label: "above",
-                        y: 0,
-                    },
-                ];
-
-                expect(
-                    markers[
-                        LabelImage.navigateToMarkerIndex(
-                            navigateDirecton,
-                            markers,
-                            0,
-                        )
-                    ].label,
-                ).toEqual("highest");
+                it("should navigate to marker above", function () {
+                    const markers = [
+                        {
+                            ...marker,
+                            label: "this",
+                            y: 0,
+                        },
path: packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts
user: login: benchristel
body: > if there is additional information in markers (like answers), then it will still be able to function correctly, right?

Yep!
created_at: 2025-05-21T19:26:59Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-15T22:33:15Z
body: **Size Change:** +80 B (+0.02%) 

**Total Size:** 471 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `packages/perseus/dist/es/index.js` | 202 kB | +80 B (+0.04%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `packages/kas/dist/es/index.js` | 20.7 kB |
| `packages/keypad-context/dist/es/index.js` | 1 kB |
| `packages/kmath/dist/es/index.js` | 5.98 kB |
| `packages/math-input/dist/es/index.js` | 98.6 kB |
| `packages/math-input/dist/es/strings.js` | 1.61 kB |
| `packages/perseus-core/dist/es/index.js` | 19.5 kB |
| `packages/perseus-editor/dist/es/index.js` | 89.6 kB |
| `packages/perseus-linter/dist/es/index.js` | 7.05 kB |
| `packages/perseus-score/dist/es/index.js` | 9.15 kB |
| `packages/perseus-utils/dist/es/index.js` | 403 B |
| `packages/perseus/dist/es/strings.js` | 7.61 kB |
| `packages/pure-markdown/dist/es/index.js` | 1.22 kB |
| `packages/simple-markdown/dist/es/index.js` | 6.72 kB |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-15T22:33:55Z
body: # npm Snapshot: Published

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

Example:
```sh
pnpm add @khanacademy/perseus@PR2495
```

If you are working in Khan Academy's webapp, you can run the below command. 
If this is for backend code, change the static flag (-s) from y/yes to n/no.
```sh
./dev/tools/bump_perseus_version.js -s y -t PR2495
```
author_association: CONTRIBUTOR
user: login: handeyeco
created_at: 2025-05-19T14:34:50Z
body: > Instead of completely removing answers from the markers when getting the answerless data, we can replace answers with an empty array

I'm hesitant about this suggestion. I think it makes sense from a code simplification perspective, but I think it would be more confusing to future humans.

1. It's inconsistent with how we've done the other widgets (IG, Matrix, NumberLine, Table, etc; although to be my own "devil's advocate" NumericInput keeps `answers`)
2. In my head, it feels conceptually misleading. `answers` with an empty array means there are no answers; `answers` getting removed means the answers were removed.
3. Some of the code simplification you suggested is because the code is trying to call post-scoring logic with pre-scoring data; I think the remedy for this is not to provide defaults for missing data, but to not allow the calling of post-scoring logic with pre-scoring data.

Not a hill I'd die on though. My main concern is consistency.
author_association: CONTRIBUTOR
user: login: Myranae
created_at: 2025-05-19T16:12:26Z
body: @benchristel @handeyeco Thanks for your input! I do feel like keeping answers with an empty array is misleading and would prefer to be in alignment with how the majority of widgets handle answers. 

Is there a reason to change to the empty array other than to simplify things? Could removing the answers field cause any issues or break anything down the line? If we decide to move forward with removing the answers field, do either of you have any suggestions for changes or feedback otherwise? @handeyeco, do you see someplace where we need to "not allow the calling of post-scoring logic with pre-scoring data" in this current PR?

Thank you both!
author_association: CONTRIBUTOR