@khanacademy/perseus 17.0.0 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/dependencies.d.ts +1 -0
  3. package/dist/es/index.css +20 -0
  4. package/dist/es/index.js +443 -338
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +20 -0
  7. package/dist/index.js +444 -340
  8. package/dist/index.js.map +1 -1
  9. package/dist/perseus-types.d.ts +1 -0
  10. package/dist/strings.js +1 -0
  11. package/dist/widgets/label-image/answer-pill.d.ts +10 -0
  12. package/dist/widgets/label-image/hide-answers-toggle.d.ts +6 -0
  13. package/dist/widgets/label-image/marker.d.ts +6 -9
  14. package/dist/widgets/label-image/types.d.ts +1 -0
  15. package/dist/widgets/label-image.d.ts +101 -12
  16. package/package.json +30 -22
  17. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +3 -3
  18. package/src/dependencies.ts +4 -0
  19. package/src/perseus-types.ts +2 -0
  20. package/src/styles/widgets/label-image.less +32 -0
  21. package/src/widgets/__stories__/label-image.stories.tsx +73 -3
  22. package/src/widgets/__testdata__/label-image.testdata.ts +118 -1
  23. package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +2 -2
  24. package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +62 -0
  25. package/src/widgets/label-image/__tests__/answer-pill.test.tsx +83 -0
  26. package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +39 -0
  27. package/src/widgets/label-image/answer-choices.tsx +7 -1
  28. package/src/widgets/label-image/answer-pill.tsx +89 -0
  29. package/src/widgets/label-image/hide-answers-toggle.tsx +40 -0
  30. package/src/widgets/label-image/marker.tsx +116 -174
  31. package/src/widgets/label-image/types.ts +1 -0
  32. package/src/widgets/label-image.test.ts +142 -52
  33. package/src/widgets/label-image.tsx +212 -135
  34. package/tsconfig-build.tsbuildinfo +1 -1
@@ -6,11 +6,12 @@
6
6
  */
7
7
 
8
8
  import Color from "@khanacademy/wonder-blocks-color";
9
+ import {View, type StyleType} from "@khanacademy/wonder-blocks-core";
9
10
  import {StyleSheet, css} from "aphrodite";
10
11
  import * as React from "react";
11
12
 
12
13
  import Icon from "../../components/icon";
13
- import {iconCheck, iconMinus} from "../../icon-paths";
14
+ import {iconCheck, iconChevronDown, iconMinus} from "../../icon-paths";
14
15
 
15
16
  import type {InteractiveMarkerType} from "./types";
16
17
 
@@ -20,16 +21,19 @@ type Props = InteractiveMarkerType & {
20
21
  // Whether this marker should pulsate to draw user attention.
21
22
  showPulsate: boolean;
22
23
  // Callbacks for when marker is interacted with using input device.
23
- onClick: (e: MouseEvent) => void;
24
- onKeyDown: (e: KeyboardEvent) => void;
24
+ onClick: (e: React.MouseEvent) => void;
25
+ onKeyDown: (e: React.KeyboardEvent) => void;
26
+ onFocus: () => void;
27
+ onBlur: () => void;
28
+ focused: boolean;
25
29
  };
26
30
 
27
- type State = {
28
- // Whether the marker button has input focus.
29
- isFocused: boolean;
30
- };
31
+ function shouldReduceMotion(): boolean {
32
+ const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
33
+ return !mediaQuery || mediaQuery.matches;
34
+ }
31
35
 
32
- export default class Marker extends React.Component<Props, State> {
36
+ export default class Marker extends React.Component<Props> {
33
37
  // The marker icon element.
34
38
  _icon: HTMLElement | null | undefined;
35
39
 
@@ -39,34 +43,20 @@ export default class Marker extends React.Component<Props, State> {
39
43
  selected: [],
40
44
  };
41
45
 
42
- state: State = {
43
- isFocused: false,
44
- };
45
-
46
- handleFocus() {
47
- this.setState({isFocused: true});
48
- }
49
-
50
- handleBlur() {
51
- this.setState({isFocused: false});
52
- }
53
-
54
46
  renderIcon(): React.ReactElement<React.ComponentProps<"div">> {
55
- const {selected, showCorrectness, showSelected, showPulsate} =
47
+ const {selected, showCorrectness, showSelected, showPulsate, focused} =
56
48
  this.props;
57
49
 
58
- const {isFocused} = this.state;
59
-
60
50
  // Only a single marker may be "selected" at a time.
61
51
  // `showSelected` is a controlled prop, that may be set to `true` for
62
52
  // one marker at a time.
63
- // `isFocused` is a controlled state, driven by focus events, and may
53
+ // `focused` is a controlled state, driven by focus events, and may
64
54
  // only be `true` when there's no answer choices popup visible, and
65
55
  // keyboard focus is given to the marker.
66
- const isSelected = showSelected || isFocused;
56
+ const isSelected = showSelected || focused;
67
57
 
68
- let innerIcon;
69
- let iconStyles;
58
+ let innerIcon: React.ReactElement | undefined;
59
+ let iconStyles: StyleType;
70
60
 
71
61
  if (showCorrectness) {
72
62
  innerIcon = (
@@ -79,35 +69,52 @@ export default class Marker extends React.Component<Props, State> {
79
69
 
80
70
  if (showCorrectness === "correct") {
81
71
  iconStyles = [
72
+ styles.markerGraded,
82
73
  styles.markerCorrect,
83
- isSelected && styles.markerCorrectSelected,
74
+ isSelected && styles.markerSelected,
84
75
  ];
85
76
  } else {
86
77
  iconStyles = [
78
+ styles.markerGraded,
87
79
  styles.markerIncorrect,
88
- isSelected && styles.markerIncorrectSelected,
80
+ isSelected && styles.markerSelected,
89
81
  ];
90
82
  }
83
+ } else if (focused) {
84
+ iconStyles = [
85
+ styles.markerFocused,
86
+ selected && selected.length > 0 && styles.markerFilled,
87
+ showSelected && styles.markerSelected,
88
+ ];
91
89
  } else if (selected && selected.length > 0) {
92
90
  iconStyles = [
93
91
  styles.markerFilled,
94
- isSelected && styles.markerFilledSelected,
92
+ isSelected && styles.markerSelected,
95
93
  ];
94
+ } else if (isSelected) {
95
+ iconStyles = [styles.markerSelected];
96
96
  } else {
97
97
  iconStyles = [
98
- isSelected
99
- ? styles.markerUnfilledSelected
100
- : showPulsate && styles.markerUnfilledPulsate,
98
+ styles.markerPulsateBase,
99
+ shouldReduceMotion()
100
+ ? showPulsate && styles.markerUnfilledPulsateOnce
101
+ : showPulsate && styles.markerUnfilledPulsateInfinite,
101
102
  ];
102
103
  }
103
104
 
105
+ if (isSelected && selected && selected.length === 0) {
106
+ innerIcon = (
107
+ <Icon icon={iconChevronDown} size={8} color={Color.white} />
108
+ );
109
+ }
110
+
104
111
  return (
105
- <div
106
- className={css(styles.markerIcon, ...iconStyles)}
112
+ <View
113
+ style={[styles.markerIcon, iconStyles]}
107
114
  ref={(node) => (this._icon = node)}
108
115
  >
109
116
  {innerIcon}
110
- </div>
117
+ </View>
111
118
  );
112
119
  }
113
120
 
@@ -131,11 +138,12 @@ export default class Marker extends React.Component<Props, State> {
131
138
  top: `${y}%`,
132
139
  }}
133
140
  tabIndex={isDisabled ? -1 : 0}
134
- onFocus={() => this.handleFocus()}
135
- onBlur={() => this.handleBlur()}
136
- // @ts-expect-error - TS2345 - Argument of type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to parameter of type 'MouseEvent'.
137
- onClick={(e) => this.props.onClick(e)}
138
- // @ts-expect-error - TS2345 - Argument of type 'KeyboardEvent<HTMLButtonElement>' is not assignable to parameter of type 'KeyboardEvent'.
141
+ onFocus={this.props.onFocus}
142
+ onBlur={this.props.onBlur}
143
+ onClick={(e) => {
144
+ e.preventDefault();
145
+ this.props.onClick(e);
146
+ }}
139
147
  onKeyDown={(e) => this.props.onKeyDown(e)}
140
148
  >
141
149
  {this.renderIcon()}
@@ -144,16 +152,10 @@ export default class Marker extends React.Component<Props, State> {
144
152
  }
145
153
  }
146
154
 
147
- const markerColor = "#1865f2";
148
- const selectedColor = "#2552b0";
149
- const activeColor = selectedColor;
150
- const correctColor = "#00a60e";
151
- const correctActiveColor = "#167b1f";
152
- const incorrectColor = "#909195";
153
- const incorrectActiveColor = "#6c6e73";
154
- const markerShadowColor = "rgba(33, 36, 44, 0.32)";
155
155
  const lightShadowColor = "rgba(33, 36, 44, 0.16)";
156
156
 
157
+ const markerSize = 20;
158
+
157
159
  const styles = StyleSheet.create({
158
160
  unstyledButton: {
159
161
  padding: 0,
@@ -171,10 +173,13 @@ const styles = StyleSheet.create({
171
173
 
172
174
  marker: {
173
175
  position: "absolute",
176
+ outline: "none",
174
177
 
175
178
  // Center marker position based on it's maximum size.
176
- width: 30,
177
- height: 30,
179
+ width: markerSize,
180
+ height: markerSize,
181
+ backgroundColor: Color.white,
182
+ borderRadius: 30,
178
183
  marginLeft: -15,
179
184
  marginTop: -15,
180
185
  },
@@ -186,180 +191,117 @@ const styles = StyleSheet.create({
186
191
  // The base and unfilled marker style.
187
192
  markerIcon: {
188
193
  display: "flex",
189
- position: "relative",
194
+ alignItems: "center",
195
+ justifyContent: "center",
190
196
 
191
197
  boxSizing: "content-box",
192
198
 
193
- width: 16,
194
- height: 16,
195
- // Center icon within marker.
196
- marginLeft: 5,
199
+ width: markerSize,
200
+ height: markerSize,
201
+
202
+ marginLeft: 2,
197
203
 
198
204
  cursor: "pointer",
199
205
 
200
- backgroundColor: markerColor,
206
+ border: `2px solid ${Color.offBlack64}`,
207
+ borderRadius: markerSize,
201
208
 
202
- border: "solid 2px #ffffff",
203
- borderRadius: 16,
209
+ boxShadow: `0 8px 8px ${Color.offBlack8}`,
204
210
 
205
- boxShadow: `0 2px 6px 0 ${markerShadowColor}`,
211
+ ":hover": {
212
+ outline: `2px solid ${Color.blue}`,
213
+ outlineOffset: 2,
214
+ },
206
215
  },
207
216
 
208
- markerUnfilledPulsate: {
217
+ // The animation that presents the marker to the learner
218
+ markerPulsateBase: {
209
219
  animationName: {
210
220
  "0%": {
211
221
  transform: "scale(1)",
222
+ backgroundColor: Color.blue,
212
223
  },
213
224
 
214
225
  "100%": {
215
- transform: "scale(1.5)",
226
+ transform: "scale(1.3)",
227
+ backgroundColor: Color.blue,
216
228
  },
217
229
  },
218
230
 
219
231
  animationDirection: "alternate",
220
232
  animationDuration: "0.8s",
221
- animationIterationCount: "infinite",
222
233
  animationTimingFunction: "ease-in",
223
234
 
224
235
  transformOrigin: "50% 50%",
225
- },
226
-
227
- markerUnfilledSelected: {
228
- "::before": {
229
- content: "''",
230
- display: "inline-block",
231
- position: "absolute",
232
-
233
- width: 20,
234
- height: 20,
235
- marginLeft: -4,
236
- marginTop: -4,
237
-
238
- border: `solid 2px ${selectedColor}`,
239
- borderRadius: 20,
240
- },
241
236
 
242
- ":active": {
243
- backgroundColor: activeColor,
237
+ animationIterationCount: "0",
238
+ },
244
239
 
245
- boxShadow: "none",
240
+ markerUnfilledPulsateInfinite: {
241
+ animationIterationCount: "infinite",
246
242
 
247
- "::before": {
248
- display: "none",
249
- },
243
+ ":hover": {
244
+ outline: `0px solid ${Color.blue}`,
250
245
  },
251
246
  },
252
247
 
253
- markerFilled: {
254
- width: 8,
255
- height: 8,
256
- // Center icon within marker.
257
- marginLeft: 9,
258
-
259
- borderRadius: 8,
260
-
261
- boxShadow: `0 1px 1px 0 ${lightShadowColor}`,
248
+ markerUnfilledPulsateOnce: {
249
+ // Doing the animation twice lets it ease-in and ease-out
250
+ animationIterationCount: "2",
262
251
  },
263
252
 
264
- markerFilledSelected: {
265
- "::before": {
266
- content: "''",
267
- display: "inline-block",
268
- position: "absolute",
269
-
270
- width: 12,
271
- height: 12,
272
- marginLeft: -4,
273
- marginTop: -4,
274
-
275
- border: `solid 2px ${selectedColor}`,
276
- borderRadius: 12,
277
- },
253
+ markerFocused: {
254
+ outline: `2px solid ${Color.blue}`,
255
+ outlineOffset: 2,
256
+ },
278
257
 
279
- ":active": {
280
- backgroundColor: activeColor,
258
+ // The learner is making an initial selection
259
+ markerSelected: {
260
+ boxShadow: `0 8px 8px ${Color.offBlack8}`,
281
261
 
282
- boxShadow: "none",
262
+ border: `solid 4px ${Color.white}`,
263
+ backgroundColor: Color.blue,
264
+ borderRadius: markerSize,
265
+ transform: "rotate(180deg)",
283
266
 
284
- "::before": {
285
- display: "none",
286
- },
267
+ ":hover": {
268
+ outline: `2px solid ${Color.blue}`,
269
+ outlineOffset: -2,
287
270
  },
288
271
  },
289
272
 
290
- markerCorrect: {
291
- width: 24,
292
- height: 24,
293
- marginLeft: 1,
294
- marginTop: 1,
273
+ // The learner has made a selection
274
+ markerFilled: {
275
+ width: markerSize - 2,
276
+ height: markerSize - 2,
277
+ borderRadius: markerSize - 2,
278
+ backgroundColor: "#ECF3FE",
295
279
 
296
- justifyContent: "center",
297
- alignItems: "center",
280
+ marginLeft: 3,
281
+ marginTop: 0,
298
282
 
299
- background: correctColor,
283
+ border: `4px solid ${Color.blue}`,
300
284
 
301
285
  boxShadow: `0 1px 1px 0 ${lightShadowColor}`,
302
286
  },
303
287
 
304
- markerCorrectSelected: {
305
- "::before": {
306
- content: "''",
307
- display: "inline-block",
308
- position: "absolute",
309
-
310
- width: 28,
311
- height: 28,
312
-
313
- border: `solid 2px ${selectedColor}`,
314
- borderRadius: 28,
315
- },
316
-
317
- ":active": {
318
- backgroundColor: correctActiveColor,
319
-
320
- boxShadow: "none",
321
-
322
- "::before": {
323
- display: "none",
324
- },
325
- },
326
- },
327
-
328
- markerIncorrect: {
329
- width: 24,
330
- height: 24,
288
+ markerGraded: {
289
+ width: markerSize,
290
+ height: markerSize,
331
291
  marginLeft: 1,
332
292
  marginTop: 1,
333
293
 
334
294
  justifyContent: "center",
335
295
  alignItems: "center",
336
-
337
- background: incorrectColor,
338
-
339
- boxShadow: `0 1px 1px 0 ${lightShadowColor}`,
296
+ border: `2px solid ${Color.white}`,
297
+ boxShadow: `0 8px 8px ${Color.offBlack8}`,
340
298
  },
341
299
 
342
- markerIncorrectSelected: {
343
- "::before": {
344
- content: "''",
345
- display: "inline-block",
346
- position: "absolute",
347
-
348
- width: 28,
349
- height: 28,
350
-
351
- border: `solid 2px ${selectedColor}`,
352
- borderRadius: 28,
353
- },
354
-
355
- ":active": {
356
- backgroundColor: incorrectActiveColor,
357
-
358
- boxShadow: "none",
300
+ markerCorrect: {
301
+ background: "#00880b", // WB green darkened by 18%
302
+ },
359
303
 
360
- "::before": {
361
- display: "none",
362
- },
363
- },
304
+ markerIncorrect: {
305
+ background: Color.offBlack64,
364
306
  },
365
307
  });
@@ -15,6 +15,7 @@ export type InteractiveMarkerType = MarkerType & {
15
15
  selected?: ReadonlyArray<string>;
16
16
  // Reveal the correctness state of the user selected answers for the marker.
17
17
  showCorrectness?: "correct" | "incorrect";
18
+ focused?: boolean;
18
19
  };
19
20
 
20
21
  // Question state for marker as result of user selected answers.
@@ -1,6 +1,17 @@
1
- import LabelImageWidget from "./label-image";
1
+ import {screen} from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
2
3
 
3
- const LabelImage = LabelImageWidget.widget;
4
+ import {
5
+ testDependencies,
6
+ testDependenciesV2,
7
+ } from "../../../../testing/test-dependencies";
8
+ import * as Dependencies from "../dependencies";
9
+
10
+ import {textQuestion} from "./__testdata__/label-image.testdata";
11
+ import {renderQuestion} from "./__tests__/renderQuestion";
12
+ import {LabelImage} from "./label-image";
13
+
14
+ import "@testing-library/jest-dom";
4
15
 
5
16
  const emptyMarker = {
6
17
  label: "",
@@ -236,67 +247,75 @@ describe("LabelImage", function () {
236
247
 
237
248
  describe("imageSideForMarkerPosition", function () {
238
249
  it("should return side: top", function () {
239
- expect(LabelImage.imageSideForMarkerPosition(21, 0)).toEqual("top");
240
- expect(LabelImage.imageSideForMarkerPosition(79, 0)).toEqual("top");
241
- expect(LabelImage.imageSideForMarkerPosition(50, 0)).toEqual("top");
242
- expect(LabelImage.imageSideForMarkerPosition(50, 25)).toEqual(
243
- "top",
244
- );
245
- expect(LabelImage.imageSideForMarkerPosition(50, 50)).toEqual(
246
- "top",
247
- );
250
+ expect(
251
+ LabelImage.imageSideForMarkerPosition(21, 0, "NONE"),
252
+ ).toEqual("top");
253
+ expect(
254
+ LabelImage.imageSideForMarkerPosition(79, 0, "NONE"),
255
+ ).toEqual("top");
256
+ expect(
257
+ LabelImage.imageSideForMarkerPosition(50, 0, "NONE"),
258
+ ).toEqual("top");
259
+ expect(
260
+ LabelImage.imageSideForMarkerPosition(50, 25, "NONE"),
261
+ ).toEqual("top");
262
+ expect(
263
+ LabelImage.imageSideForMarkerPosition(50, 50, "NONE"),
264
+ ).toEqual("top");
248
265
  });
249
266
 
250
267
  it("should return side: right", function () {
251
- expect(LabelImage.imageSideForMarkerPosition(100, 0)).toEqual(
252
- "right",
253
- );
254
- expect(LabelImage.imageSideForMarkerPosition(100, 100)).toEqual(
255
- "right",
256
- );
257
- expect(LabelImage.imageSideForMarkerPosition(100, 50)).toEqual(
258
- "right",
259
- );
260
- expect(LabelImage.imageSideForMarkerPosition(75, 50)).toEqual(
261
- "right",
262
- );
263
- expect(LabelImage.imageSideForMarkerPosition(51, 50)).toEqual(
264
- "right",
265
- );
268
+ expect(
269
+ LabelImage.imageSideForMarkerPosition(100, 0, "NONE"),
270
+ ).toEqual("right");
271
+ expect(
272
+ LabelImage.imageSideForMarkerPosition(100, 100, "NONE"),
273
+ ).toEqual("right");
274
+ expect(
275
+ LabelImage.imageSideForMarkerPosition(100, 50, "NONE"),
276
+ ).toEqual("right");
277
+ expect(
278
+ LabelImage.imageSideForMarkerPosition(75, 50, "NONE"),
279
+ ).toEqual("right");
280
+ expect(
281
+ LabelImage.imageSideForMarkerPosition(51, 50, "NONE"),
282
+ ).toEqual("right");
266
283
  });
267
284
 
268
285
  it("should return side: bottom", function () {
269
- expect(LabelImage.imageSideForMarkerPosition(21, 100)).toEqual(
270
- "bottom",
271
- );
272
- expect(LabelImage.imageSideForMarkerPosition(79, 100)).toEqual(
273
- "bottom",
274
- );
275
- expect(LabelImage.imageSideForMarkerPosition(50, 100)).toEqual(
276
- "bottom",
277
- );
278
- expect(LabelImage.imageSideForMarkerPosition(50, 75)).toEqual(
279
- "bottom",
280
- );
281
- expect(LabelImage.imageSideForMarkerPosition(50, 51)).toEqual(
282
- "bottom",
283
- );
286
+ expect(
287
+ LabelImage.imageSideForMarkerPosition(21, 100, "NONE"),
288
+ ).toEqual("bottom");
289
+ expect(
290
+ LabelImage.imageSideForMarkerPosition(79, 100, "NONE"),
291
+ ).toEqual("bottom");
292
+ expect(
293
+ LabelImage.imageSideForMarkerPosition(50, 100, "NONE"),
294
+ ).toEqual("bottom");
295
+ expect(
296
+ LabelImage.imageSideForMarkerPosition(50, 75, "NONE"),
297
+ ).toEqual("bottom");
298
+ expect(
299
+ LabelImage.imageSideForMarkerPosition(50, 51, "NONE"),
300
+ ).toEqual("bottom");
284
301
  });
285
302
 
286
303
  it("should return side: left", function () {
287
- expect(LabelImage.imageSideForMarkerPosition(0, 0)).toEqual("left");
288
- expect(LabelImage.imageSideForMarkerPosition(0, 100)).toEqual(
289
- "left",
290
- );
291
- expect(LabelImage.imageSideForMarkerPosition(0, 50)).toEqual(
292
- "left",
293
- );
294
- expect(LabelImage.imageSideForMarkerPosition(25, 50)).toEqual(
295
- "left",
296
- );
297
- expect(LabelImage.imageSideForMarkerPosition(49, 50)).toEqual(
304
+ expect(LabelImage.imageSideForMarkerPosition(0, 0, "NONE")).toEqual(
298
305
  "left",
299
306
  );
307
+ expect(
308
+ LabelImage.imageSideForMarkerPosition(0, 100, "NONE"),
309
+ ).toEqual("left");
310
+ expect(
311
+ LabelImage.imageSideForMarkerPosition(0, 50, "NONE"),
312
+ ).toEqual("left");
313
+ expect(
314
+ LabelImage.imageSideForMarkerPosition(25, 50, "NONE"),
315
+ ).toEqual("left");
316
+ expect(
317
+ LabelImage.imageSideForMarkerPosition(49, 50, "NONE"),
318
+ ).toEqual("left");
300
319
  });
301
320
  });
302
321
 
@@ -837,4 +856,75 @@ describe("LabelImage", function () {
837
856
  });
838
857
  });
839
858
  });
859
+
860
+ describe("analytics", () => {
861
+ beforeEach(() => {
862
+ jest.spyOn(testDependenciesV2.analytics, "onAnalyticsEvent");
863
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
864
+ testDependencies,
865
+ );
866
+ jest.spyOn(Dependencies, "useDependencies").mockReturnValue(
867
+ testDependenciesV2,
868
+ );
869
+ });
870
+
871
+ it("sends an analytics event when the toggle is interacted with", async () => {
872
+ // render component
873
+ renderQuestion(textQuestion);
874
+
875
+ // Toggle the switch
876
+ const toggleAnswerSwitch = await screen.findByRole("switch");
877
+ userEvent.click(toggleAnswerSwitch);
878
+
879
+ expect(
880
+ testDependenciesV2.analytics.onAnalyticsEvent,
881
+ ).toHaveBeenCalledWith({
882
+ type: "perseus:label-image:toggle-answers-hidden",
883
+ payload: null,
884
+ });
885
+ });
886
+
887
+ it("sends an analytics event when a marker is interacted with", async () => {
888
+ // render component
889
+ renderQuestion(textQuestion);
890
+
891
+ // Toggle the button
892
+ const markerButton = await screen.findByLabelText(
893
+ "The fourth unlabeled bar line.",
894
+ );
895
+ userEvent.click(markerButton);
896
+
897
+ expect(
898
+ testDependenciesV2.analytics.onAnalyticsEvent,
899
+ ).toHaveBeenCalledWith({
900
+ type: "perseus:label-image:marker-interacted-with",
901
+ payload: null,
902
+ });
903
+ });
904
+
905
+ it("sends analytics when a choice is interacted with", async () => {
906
+ // render component
907
+ renderQuestion(textQuestion);
908
+
909
+ // Toggle the marker
910
+ const markerButton = await screen.findByLabelText(
911
+ "The fourth unlabeled bar line.",
912
+ );
913
+ userEvent.click(markerButton);
914
+
915
+ await screen.findByRole("dialog");
916
+
917
+ // Select a choice
918
+ const choices = await screen.findAllByText("Trucks");
919
+ const choice = choices[choices.length - 1];
920
+ userEvent.click(choice);
921
+
922
+ expect(
923
+ testDependenciesV2.analytics.onAnalyticsEvent,
924
+ ).toHaveBeenCalledWith({
925
+ type: "perseus:label-image:choiced-interacted-with",
926
+ payload: null,
927
+ });
928
+ });
929
+ });
840
930
  });