@khanacademy/perseus 19.1.4 → 19.1.6

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 (487) hide show
  1. package/dist/components/graphie.d.ts +9 -1
  2. package/dist/es/index.js +255 -122
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.js +261 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/interactive2/interactive-util.d.ts +1 -1
  7. package/dist/interactive2/movable-point-options.d.ts +5 -4
  8. package/dist/interactive2/movable.d.ts +90 -13
  9. package/dist/interactive2/types.d.ts +6 -0
  10. package/dist/util/graphie.d.ts +5 -0
  11. package/dist/widgets/expression.d.ts +4 -4
  12. package/package.json +11 -8
  13. package/.babelrc.js +0 -8
  14. package/.eslintrc.js +0 -12
  15. package/CHANGELOG.md +0 -1422
  16. package/src/__stories__/article-renderer.stories.tsx +0 -100
  17. package/src/__stories__/hints-renderer.stories.tsx +0 -55
  18. package/src/__stories__/server-item-renderer.stories.tsx +0 -44
  19. package/src/__testdata__/article-renderer.testdata.ts +0 -212
  20. package/src/__testdata__/graphie.testdata.ts +0 -51
  21. package/src/__testdata__/renderer.testdata.ts +0 -274
  22. package/src/__testdata__/server-item-renderer.testdata.ts +0 -171
  23. package/src/__tests__/__snapshots__/perseus-markdown.test.ts.snap +0 -507
  24. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +0 -525
  25. package/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap +0 -285
  26. package/src/__tests__/a11y.test.ts +0 -303
  27. package/src/__tests__/article-renderer.test.tsx +0 -130
  28. package/src/__tests__/error-boundary.test.tsx +0 -76
  29. package/src/__tests__/extract-perseus-data.test.ts +0 -1027
  30. package/src/__tests__/init.test.ts +0 -10
  31. package/src/__tests__/jipt-paragraphs.test.ts +0 -32
  32. package/src/__tests__/mock-asset-loading-widget.tsx +0 -53
  33. package/src/__tests__/mock-widget.tsx +0 -33
  34. package/src/__tests__/perseus-markdown.test.ts +0 -764
  35. package/src/__tests__/renderability.test.ts +0 -480
  36. package/src/__tests__/renderer-api.test.tsx +0 -302
  37. package/src/__tests__/renderer.test.tsx +0 -1816
  38. package/src/__tests__/server-item-renderer.test.tsx +0 -582
  39. package/src/__tests__/test-items/image-item.ts +0 -24
  40. package/src/__tests__/test-items/input-number-1-item.ts +0 -26
  41. package/src/__tests__/test-items/input-number-2-item.ts +0 -38
  42. package/src/__tests__/test-items/table-item.ts +0 -29
  43. package/src/__tests__/tex-wrangler.test.ts +0 -82
  44. package/src/__tests__/util.test.ts +0 -40
  45. package/src/__tests__/widget-container.test.tsx +0 -126
  46. package/src/__tests__/widgets.test.ts +0 -91
  47. package/src/a11y.ts +0 -45
  48. package/src/all-widgets.ts +0 -9
  49. package/src/article-renderer.tsx +0 -237
  50. package/src/asset-context.ts +0 -30
  51. package/src/basic-widgets.ts +0 -16
  52. package/src/components/__stories__/button-group.stories.tsx +0 -55
  53. package/src/components/__stories__/fixed-to-responsive.stories.tsx +0 -141
  54. package/src/components/__stories__/graph.stories.tsx +0 -28
  55. package/src/components/__stories__/graphie.stories.tsx +0 -33
  56. package/src/components/__stories__/hud.stories.tsx +0 -35
  57. package/src/components/__stories__/icon.stories.tsx +0 -63
  58. package/src/components/__stories__/image-loader.stories.tsx +0 -63
  59. package/src/components/__stories__/info-tip.stories.tsx +0 -31
  60. package/src/components/__stories__/inline-icon.stories.tsx +0 -40
  61. package/src/components/__stories__/input-with-examples.stories.tsx +0 -53
  62. package/src/components/__stories__/lint.stories.tsx +0 -86
  63. package/src/components/__stories__/math-input.stories.tsx +0 -43
  64. package/src/components/__stories__/math-output.stories.tsx +0 -23
  65. package/src/components/__stories__/multi-button-group.stories.tsx +0 -66
  66. package/src/components/__stories__/number-input.stories.tsx +0 -41
  67. package/src/components/__stories__/prop-check-box.stories.tsx +0 -65
  68. package/src/components/__stories__/range-input.stories.tsx +0 -29
  69. package/src/components/__stories__/simple-keypad-input.stories.tsx +0 -27
  70. package/src/components/__stories__/sortable.stories.tsx +0 -77
  71. package/src/components/__stories__/stub-tag-editor.stories.tsx +0 -45
  72. package/src/components/__stories__/svg-image.stories.tsx +0 -74
  73. package/src/components/__stories__/tex.stories.tsx +0 -23
  74. package/src/components/__stories__/text-input.stories.tsx +0 -33
  75. package/src/components/__stories__/text-list-editor.stories.tsx +0 -32
  76. package/src/components/__stories__/tooltip.stories.tsx +0 -52
  77. package/src/components/__stories__/zoomable-tex.stories.tsx +0 -41
  78. package/src/components/__stories__/zoomable.stories.tsx +0 -49
  79. package/src/components/__testdata__/sorter.testdata.ts +0 -19
  80. package/src/components/__tests__/__snapshots__/sortable.test.tsx.snap +0 -163
  81. package/src/components/__tests__/__snapshots__/sorter.test.tsx.snap +0 -265
  82. package/src/components/__tests__/math-input.test.tsx +0 -147
  83. package/src/components/__tests__/number-input.test.tsx +0 -128
  84. package/src/components/__tests__/sortable.test.tsx +0 -269
  85. package/src/components/__tests__/sorter.test.tsx +0 -114
  86. package/src/components/__tests__/zoomable.test.tsx +0 -399
  87. package/src/components/button-group.tsx +0 -130
  88. package/src/components/constants.ts +0 -14
  89. package/src/components/fixed-to-responsive.tsx +0 -169
  90. package/src/components/graph.tsx +0 -444
  91. package/src/components/graphie-classes.ts +0 -153
  92. package/src/components/graphie-movables.ts +0 -338
  93. package/src/components/graphie.tsx +0 -392
  94. package/src/components/highlighting/highlightable-content.tsx +0 -240
  95. package/src/components/highlighting/highlights.ts +0 -207
  96. package/src/components/highlighting/ranges.ts +0 -304
  97. package/src/components/highlighting/types.ts +0 -73
  98. package/src/components/highlighting/ui/highlight-renderer.tsx +0 -172
  99. package/src/components/highlighting/ui/highlight-set-renderer.tsx +0 -185
  100. package/src/components/highlighting/ui/highlight-tooltip.tsx +0 -141
  101. package/src/components/highlighting/ui/highlighting-ui.tsx +0 -143
  102. package/src/components/highlighting/ui/selection-tracker.tsx +0 -186
  103. package/src/components/highlighting/ui/types.ts +0 -21
  104. package/src/components/highlighting/ui/util.ts +0 -165
  105. package/src/components/highlighting/word-indexer.tsx +0 -152
  106. package/src/components/hud.tsx +0 -167
  107. package/src/components/icon.tsx +0 -141
  108. package/src/components/image-loader.tsx +0 -177
  109. package/src/components/info-tip/info-tip.tsx +0 -144
  110. package/src/components/info-tip.tsx +0 -39
  111. package/src/components/inline-icon.tsx +0 -60
  112. package/src/components/input-with-examples.tsx +0 -249
  113. package/src/components/lint.tsx +0 -459
  114. package/src/components/math-input.tsx +0 -474
  115. package/src/components/math-output.tsx +0 -134
  116. package/src/components/multi-button-group.tsx +0 -135
  117. package/src/components/number-input.tsx +0 -249
  118. package/src/components/prop-check-box.tsx +0 -79
  119. package/src/components/range-input.tsx +0 -60
  120. package/src/components/simple-keypad-input.tsx +0 -88
  121. package/src/components/sortable.tsx +0 -958
  122. package/src/components/stub-tag-editor.tsx +0 -53
  123. package/src/components/svg-image.tsx +0 -779
  124. package/src/components/tex.tsx +0 -67
  125. package/src/components/text-input.tsx +0 -129
  126. package/src/components/text-list-editor.tsx +0 -166
  127. package/src/components/tooltip.tsx +0 -410
  128. package/src/components/visibility-observer/__stories__/visibility-observer.stories.tsx +0 -141
  129. package/src/components/visibility-observer/create-visibility-observer.ts +0 -108
  130. package/src/components/visibility-observer/get-scroll-parent.ts +0 -46
  131. package/src/components/visibility-observer/visibility-observer.tsx +0 -124
  132. package/src/components/zoomable-tex.tsx +0 -78
  133. package/src/components/zoomable.tsx +0 -304
  134. package/src/components.ts +0 -15
  135. package/src/definition-context.tsx +0 -59
  136. package/src/dependencies.ts +0 -40
  137. package/src/error-boundary.tsx +0 -70
  138. package/src/extra-widgets.ts +0 -75
  139. package/src/hint-renderer.tsx +0 -151
  140. package/src/hints-renderer.tsx +0 -372
  141. package/src/icon-paths.ts +0 -119
  142. package/src/index.ts +0 -171
  143. package/src/init.ts +0 -56
  144. package/src/interaction-tracker.ts +0 -62
  145. package/src/interactive2/__tests__/movable-mock.ts +0 -43
  146. package/src/interactive2/__tests__/movable-point.test.ts +0 -144
  147. package/src/interactive2/__tests__/movable.test.ts +0 -157
  148. package/src/interactive2/interactive-util.test.ts +0 -93
  149. package/src/interactive2/interactive-util.ts +0 -159
  150. package/src/interactive2/movable-helper-methods.ts +0 -166
  151. package/src/interactive2/movable-line-options.ts +0 -419
  152. package/src/interactive2/movable-line.ts +0 -307
  153. package/src/interactive2/movable-point-options.ts +0 -212
  154. package/src/interactive2/movable-point.tsx +0 -585
  155. package/src/interactive2/movable-polygon-options.ts +0 -389
  156. package/src/interactive2/movable-polygon.ts +0 -360
  157. package/src/interactive2/movable.ts +0 -279
  158. package/src/interactive2/objective_.ts +0 -79
  159. package/src/interactive2/types.ts +0 -2
  160. package/src/interactive2/wrapped-defaults.ts +0 -87
  161. package/src/interactive2/wrapped-ellipse.ts +0 -94
  162. package/src/interactive2/wrapped-line.ts +0 -116
  163. package/src/interactive2/wrapped-path.ts +0 -51
  164. package/src/interactive2.ts +0 -70
  165. package/src/item-version.ts +0 -10
  166. package/src/jipt-paragraphs.ts +0 -49
  167. package/src/jquery.mobile.vmouse.js +0 -545
  168. package/src/loading-context.ts +0 -21
  169. package/src/logging/log.test.ts +0 -39
  170. package/src/logging/log.ts +0 -97
  171. package/src/mixins/__tests__/widget-prop-denylist.test.ts +0 -21
  172. package/src/mixins/changeable.ts +0 -133
  173. package/src/mixins/editor-jsonify.ts +0 -13
  174. package/src/mixins/widget-jsonify-deprecated.ts +0 -23
  175. package/src/mixins/widget-prop-denylist.ts +0 -48
  176. package/src/multi-items/__stories__/multi-renderer.stories.tsx +0 -83
  177. package/src/multi-items/__testdata__/multi-renderer.testdata.ts +0 -172
  178. package/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +0 -1164
  179. package/src/multi-items/__tests__/items.test.ts +0 -240
  180. package/src/multi-items/__tests__/multi-renderer.test.tsx +0 -914
  181. package/src/multi-items/__tests__/prop-type-builders.test.ts +0 -133
  182. package/src/multi-items/__tests__/shapes.test.ts +0 -43
  183. package/src/multi-items/__tests__/trees.test.ts +0 -317
  184. package/src/multi-items/item-types.ts +0 -56
  185. package/src/multi-items/items.ts +0 -176
  186. package/src/multi-items/multi-renderer.tsx +0 -569
  187. package/src/multi-items/prop-type-builders.ts +0 -106
  188. package/src/multi-items/shape-types.ts +0 -52
  189. package/src/multi-items/shapes.ts +0 -52
  190. package/src/multi-items/tree-types.ts +0 -47
  191. package/src/multi-items/trees.ts +0 -360
  192. package/src/multi-items.ts +0 -75
  193. package/src/perseus-api.tsx +0 -188
  194. package/src/perseus-env.ts +0 -27
  195. package/src/perseus-error.ts +0 -17
  196. package/src/perseus-markdown.tsx +0 -405
  197. package/src/perseus-types.ts +0 -1366
  198. package/src/perseus-version.ts +0 -9
  199. package/src/question-paragraph.tsx +0 -34
  200. package/src/renderability.ts +0 -109
  201. package/src/renderer.tsx +0 -2000
  202. package/src/server-item-renderer.tsx +0 -495
  203. package/src/sigfigs.ts +0 -490
  204. package/src/styles/articles.less +0 -749
  205. package/src/styles/constants.ts +0 -94
  206. package/src/styles/global-constants.ts +0 -343
  207. package/src/styles/global-styles.ts +0 -306
  208. package/src/styles/khan-exercise.css +0 -1468
  209. package/src/styles/media-queries.ts +0 -47
  210. package/src/styles/perseus-renderer.less +0 -732
  211. package/src/styles/shared.ts +0 -130
  212. package/src/styles/util.less +0 -47
  213. package/src/styles/variables.boostrap.less +0 -277
  214. package/src/styles/variables.less +0 -56
  215. package/src/styles/variables.shared.less +0 -340
  216. package/src/styles/widgets/categorizer.less +0 -38
  217. package/src/styles/widgets/dropdown.less +0 -3
  218. package/src/styles/widgets/expression.less +0 -65
  219. package/src/styles/widgets/graded-group.less +0 -36
  220. package/src/styles/widgets/grapher.less +0 -12
  221. package/src/styles/widgets/group.less +0 -19
  222. package/src/styles/widgets/image.less +0 -54
  223. package/src/styles/widgets/interactive-graph.less +0 -64
  224. package/src/styles/widgets/label-image.less +0 -57
  225. package/src/styles/widgets/matcher.less +0 -8
  226. package/src/styles/widgets/matrix.less +0 -126
  227. package/src/styles/widgets/measurer.less +0 -20
  228. package/src/styles/widgets/orderer.less +0 -98
  229. package/src/styles/widgets/passage.less +0 -154
  230. package/src/styles/widgets/plotter.less +0 -31
  231. package/src/styles/widgets/radio.less +0 -58
  232. package/src/styles/widgets/reaction-diagram.less +0 -35
  233. package/src/styles/widgets/sequence.less +0 -5
  234. package/src/styles/widgets/sortable.less +0 -113
  235. package/src/styles/widgets/table.less +0 -57
  236. package/src/styles/widgets/unit.less +0 -15
  237. package/src/tex-wrangler.ts +0 -182
  238. package/src/translation-linter.ts +0 -126
  239. package/src/traversal.ts +0 -138
  240. package/src/types.ts +0 -513
  241. package/src/util/a11y.ts +0 -23
  242. package/src/util/answer-types.test.ts +0 -114
  243. package/src/util/answer-types.ts +0 -876
  244. package/src/util/colors.ts +0 -93
  245. package/src/util/drawing-transform.test.ts +0 -67
  246. package/src/util/drawing-transform.ts +0 -102
  247. package/src/util/extract-perseus-data.ts +0 -539
  248. package/src/util/fix-passage-refs.ts +0 -121
  249. package/src/util/geometry.test.ts +0 -75
  250. package/src/util/geometry.ts +0 -312
  251. package/src/util/graph-bounds.ts +0 -23
  252. package/src/util/graph-utils.ts +0 -15
  253. package/src/util/graphie.test.ts +0 -1277
  254. package/src/util/graphie.ts +0 -1566
  255. package/src/util/interactive.ts +0 -4172
  256. package/src/util/interval.ts +0 -4
  257. package/src/util/katex-preprocess.ts +0 -14
  258. package/src/util/math.test.ts +0 -11
  259. package/src/util/math.ts +0 -294
  260. package/src/util/mobile-native-utils.test.ts +0 -22
  261. package/src/util/mobile-native-utils.ts +0 -12
  262. package/src/util/parse-perseus-json.test.ts +0 -10
  263. package/src/util/parse-perseus-json.ts +0 -13
  264. package/src/util/react-render.tsx +0 -27
  265. package/src/util/register-all-widgets-for-testing.ts +0 -13
  266. package/src/util/scroll-utils.ts +0 -155
  267. package/src/util/sizing-utils.ts +0 -46
  268. package/src/util/tex.ts +0 -73
  269. package/src/util/url-utils.ts +0 -34
  270. package/src/util.graphie.test.ts +0 -45
  271. package/src/util.graphie.ts +0 -59
  272. package/src/util.ts +0 -966
  273. package/src/version.ts +0 -10
  274. package/src/widget-container.tsx +0 -199
  275. package/src/widgets/__stories__/categorizer.stories.tsx +0 -14
  276. package/src/widgets/__stories__/cs-program.stories.tsx +0 -14
  277. package/src/widgets/__stories__/definition.stories.tsx +0 -114
  278. package/src/widgets/__stories__/dropdown.stories.tsx +0 -14
  279. package/src/widgets/__stories__/explanation.stories.tsx +0 -18
  280. package/src/widgets/__stories__/expression.stories.tsx +0 -145
  281. package/src/widgets/__stories__/graded-group-set.stories.tsx +0 -31
  282. package/src/widgets/__stories__/graded-group.stories.tsx +0 -31
  283. package/src/widgets/__stories__/grapher.stories.tsx +0 -46
  284. package/src/widgets/__stories__/group.stories.tsx +0 -14
  285. package/src/widgets/__stories__/iframe.stories.tsx +0 -14
  286. package/src/widgets/__stories__/image.stories.tsx +0 -85
  287. package/src/widgets/__stories__/input-number.stories.tsx +0 -101
  288. package/src/widgets/__stories__/interaction.stories.tsx +0 -14
  289. package/src/widgets/__stories__/interactive-graph.stories.tsx +0 -60
  290. package/src/widgets/__stories__/label-image.stories.tsx +0 -130
  291. package/src/widgets/__stories__/matcher.stories.tsx +0 -14
  292. package/src/widgets/__stories__/matrix.stories.tsx +0 -14
  293. package/src/widgets/__stories__/number-line.stories.tsx +0 -14
  294. package/src/widgets/__stories__/numeric-input.stories.tsx +0 -105
  295. package/src/widgets/__stories__/orderer.stories.tsx +0 -18
  296. package/src/widgets/__stories__/passage-ref.stories.tsx +0 -33
  297. package/src/widgets/__stories__/passage.stories.tsx +0 -39
  298. package/src/widgets/__stories__/plotter.stories.tsx +0 -27
  299. package/src/widgets/__stories__/radio.stories.tsx +0 -86
  300. package/src/widgets/__stories__/random-widgets.stories.tsx +0 -59
  301. package/src/widgets/__stories__/sorter.stories.tsx +0 -14
  302. package/src/widgets/__stories__/test-keypad-context-wrapper.tsx +0 -66
  303. package/src/widgets/__stories__/unit.stories.tsx +0 -28
  304. package/src/widgets/__stories__/video-transcript-link.stories.tsx +0 -23
  305. package/src/widgets/__stories__/video.stories.tsx +0 -22
  306. package/src/widgets/__testdata__/base-radio.testdata.ts +0 -21
  307. package/src/widgets/__testdata__/categorizer.testdata.ts +0 -89
  308. package/src/widgets/__testdata__/cs-program.testdata.ts +0 -27
  309. package/src/widgets/__testdata__/dropdown.testdata.ts +0 -71
  310. package/src/widgets/__testdata__/explanation.testdata.ts +0 -85
  311. package/src/widgets/__testdata__/expression.testdata.ts +0 -198
  312. package/src/widgets/__testdata__/graded-group-set.testdata.ts +0 -356
  313. package/src/widgets/__testdata__/graded-group.testdata.ts +0 -77
  314. package/src/widgets/__testdata__/grapher.testdata.ts +0 -374
  315. package/src/widgets/__testdata__/group.testdata.ts +0 -153
  316. package/src/widgets/__testdata__/iframe.testdata.ts +0 -35
  317. package/src/widgets/__testdata__/image.testdata.ts +0 -36
  318. package/src/widgets/__testdata__/input-number.testdata.ts +0 -72
  319. package/src/widgets/__testdata__/interaction.testdata.ts +0 -262
  320. package/src/widgets/__testdata__/interactive-graph-random.testdata.ts +0 -269
  321. package/src/widgets/__testdata__/interactive-graph.testdata.ts +0 -644
  322. package/src/widgets/__testdata__/label-image.testdata.ts +0 -308
  323. package/src/widgets/__testdata__/matcher.testdata.ts +0 -33
  324. package/src/widgets/__testdata__/matrix.testdata.ts +0 -28
  325. package/src/widgets/__testdata__/number-line.testdata.ts +0 -30
  326. package/src/widgets/__testdata__/numeric-input.testdata.ts +0 -265
  327. package/src/widgets/__testdata__/orderer.testdata.ts +0 -123
  328. package/src/widgets/__testdata__/passage-ref.testdata.ts +0 -101
  329. package/src/widgets/__testdata__/passage.testdata.ts +0 -95
  330. package/src/widgets/__testdata__/plotter.testdata.ts +0 -36
  331. package/src/widgets/__testdata__/radio.testdata.ts +0 -324
  332. package/src/widgets/__testdata__/randomizers.ts +0 -51
  333. package/src/widgets/__testdata__/sorter.testdata.ts +0 -19
  334. package/src/widgets/__testdata__/video.testdata.ts +0 -61
  335. package/src/widgets/__tests__/__snapshots__/categorizer.test.ts.snap +0 -940
  336. package/src/widgets/__tests__/__snapshots__/cs-program.test.ts.snap +0 -67
  337. package/src/widgets/__tests__/__snapshots__/definition.test.ts.snap +0 -97
  338. package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +0 -133
  339. package/src/widgets/__tests__/__snapshots__/explanation.test.ts.snap +0 -425
  340. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt.test.ts.snap +0 -729
  341. package/src/widgets/__tests__/__snapshots__/graded-group-set.test.ts.snap +0 -339
  342. package/src/widgets/__tests__/__snapshots__/graded-group.test.ts.snap +0 -801
  343. package/src/widgets/__tests__/__snapshots__/grapher.test.ts.snap +0 -2673
  344. package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +0 -1211
  345. package/src/widgets/__tests__/__snapshots__/iframe.test.ts.snap +0 -61
  346. package/src/widgets/__tests__/__snapshots__/image.test.ts.snap +0 -378
  347. package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +0 -495
  348. package/src/widgets/__tests__/__snapshots__/interaction.test.ts.snap +0 -292
  349. package/src/widgets/__tests__/__snapshots__/interactive-graph.test.ts.snap +0 -2889
  350. package/src/widgets/__tests__/__snapshots__/matcher.test.tsx.snap +0 -883
  351. package/src/widgets/__tests__/__snapshots__/matrix.test.ts.snap +0 -514
  352. package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +0 -814
  353. package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +0 -470
  354. package/src/widgets/__tests__/__snapshots__/orderer.test.ts.snap +0 -243
  355. package/src/widgets/__tests__/__snapshots__/passage-ref.test.ts.snap +0 -434
  356. package/src/widgets/__tests__/__snapshots__/passage.test.tsx.snap +0 -1956
  357. package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +0 -827
  358. package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +0 -4237
  359. package/src/widgets/__tests__/__snapshots__/unit.test.tsx.snap +0 -20
  360. package/src/widgets/__tests__/__snapshots__/video.test.ts.snap +0 -153
  361. package/src/widgets/__tests__/categorizer.test.ts +0 -203
  362. package/src/widgets/__tests__/cs-program.test.ts +0 -43
  363. package/src/widgets/__tests__/definition.test.ts +0 -136
  364. package/src/widgets/__tests__/dropdown.test.ts +0 -105
  365. package/src/widgets/__tests__/explanation.test.ts +0 -198
  366. package/src/widgets/__tests__/expression-mobile.test.tsx +0 -363
  367. package/src/widgets/__tests__/expression.test.tsx +0 -484
  368. package/src/widgets/__tests__/graded-group-set-jipt.test.ts +0 -30
  369. package/src/widgets/__tests__/graded-group-set.test.ts +0 -288
  370. package/src/widgets/__tests__/graded-group.test.ts +0 -232
  371. package/src/widgets/__tests__/grapher.cypress.ts +0 -576
  372. package/src/widgets/__tests__/grapher.test.ts +0 -33
  373. package/src/widgets/__tests__/group.test.tsx +0 -510
  374. package/src/widgets/__tests__/iframe.test.ts +0 -45
  375. package/src/widgets/__tests__/image.test.ts +0 -62
  376. package/src/widgets/__tests__/input-number.test.ts +0 -354
  377. package/src/widgets/__tests__/interaction.test.ts +0 -31
  378. package/src/widgets/__tests__/interactive-graph.cypress.ts +0 -815
  379. package/src/widgets/__tests__/interactive-graph.test.ts +0 -106
  380. package/src/widgets/__tests__/matcher.test.tsx +0 -176
  381. package/src/widgets/__tests__/matrix.test.ts +0 -82
  382. package/src/widgets/__tests__/number-line.test.ts +0 -79
  383. package/src/widgets/__tests__/numeric-input.test.ts +0 -575
  384. package/src/widgets/__tests__/orderer.test.ts +0 -73
  385. package/src/widgets/__tests__/passage-ref.test.ts +0 -168
  386. package/src/widgets/__tests__/passage.test.tsx +0 -224
  387. package/src/widgets/__tests__/plotter.test.tsx +0 -69
  388. package/src/widgets/__tests__/radio/base-radio.test.tsx +0 -470
  389. package/src/widgets/__tests__/radio/choice-icon.test.tsx +0 -90
  390. package/src/widgets/__tests__/radio/choice.test.tsx +0 -310
  391. package/src/widgets/__tests__/radio/focus-ring.test.tsx +0 -22
  392. package/src/widgets/__tests__/radio/option-status.test.tsx +0 -77
  393. package/src/widgets/__tests__/radio.test.ts +0 -807
  394. package/src/widgets/__tests__/randomizers.test.ts +0 -53
  395. package/src/widgets/__tests__/renderQuestion.tsx +0 -101
  396. package/src/widgets/__tests__/unit.test.tsx +0 -168
  397. package/src/widgets/__tests__/video.test.ts +0 -69
  398. package/src/widgets/categorizer.tsx +0 -344
  399. package/src/widgets/cs-program.tsx +0 -227
  400. package/src/widgets/definition.tsx +0 -121
  401. package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +0 -30
  402. package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +0 -70
  403. package/src/widgets/deprecated-standin/__tests__/deprecated-standin.test.ts +0 -93
  404. package/src/widgets/deprecated-standin/index.tsx +0 -58
  405. package/src/widgets/dropdown.tsx +0 -142
  406. package/src/widgets/explanation.tsx +0 -317
  407. package/src/widgets/expression.tsx +0 -707
  408. package/src/widgets/graded-group-answer-bar.tsx +0 -142
  409. package/src/widgets/graded-group-set.tsx +0 -338
  410. package/src/widgets/graded-group.tsx +0 -471
  411. package/src/widgets/grapher/util.tsx +0 -799
  412. package/src/widgets/grapher.tsx +0 -660
  413. package/src/widgets/group.tsx +0 -195
  414. package/src/widgets/iframe.tsx +0 -195
  415. package/src/widgets/image.tsx +0 -312
  416. package/src/widgets/input-number.tsx +0 -412
  417. package/src/widgets/interaction/arrow-picker.tsx +0 -38
  418. package/src/widgets/interaction/color-picker.tsx +0 -69
  419. package/src/widgets/interaction/dash-picker.tsx +0 -45
  420. package/src/widgets/interaction/element-container.tsx +0 -100
  421. package/src/widgets/interaction.tsx +0 -826
  422. package/src/widgets/interactive-graph.tsx +0 -2597
  423. package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +0 -91
  424. package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +0 -62
  425. package/src/widgets/label-image/__stories__/marker.stories.tsx +0 -171
  426. package/src/widgets/label-image/__tests__/answer-pill.test.tsx +0 -75
  427. package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +0 -39
  428. package/src/widgets/label-image/answer-choices.tsx +0 -88
  429. package/src/widgets/label-image/answer-pill.tsx +0 -103
  430. package/src/widgets/label-image/hide-answers-toggle.tsx +0 -40
  431. package/src/widgets/label-image/marker.tsx +0 -268
  432. package/src/widgets/label-image/strings.ts +0 -5
  433. package/src/widgets/label-image/types.ts +0 -27
  434. package/src/widgets/label-image.test.ts +0 -928
  435. package/src/widgets/label-image.tsx +0 -819
  436. package/src/widgets/lights-puzzle.tsx +0 -359
  437. package/src/widgets/matcher.tsx +0 -298
  438. package/src/widgets/matrix.tsx +0 -634
  439. package/src/widgets/measurer.tsx +0 -222
  440. package/src/widgets/molecule/__tests__/layout.test.ts +0 -162
  441. package/src/widgets/molecule/__tests__/parser.test.ts +0 -155
  442. package/src/widgets/molecule/molecule-drawing.ts +0 -173
  443. package/src/widgets/molecule/molecule-layout.ts +0 -362
  444. package/src/widgets/molecule/smiles-parser.ts +0 -296
  445. package/src/widgets/molecule.tsx +0 -172
  446. package/src/widgets/number-line.tsx +0 -862
  447. package/src/widgets/numeric-input.tsx +0 -576
  448. package/src/widgets/orderer.tsx +0 -786
  449. package/src/widgets/passage/__tests__/passage-markdown.test.ts +0 -376
  450. package/src/widgets/passage/passage-markdown.tsx +0 -475
  451. package/src/widgets/passage-ref-target.tsx +0 -76
  452. package/src/widgets/passage-ref.tsx +0 -213
  453. package/src/widgets/passage.tsx +0 -582
  454. package/src/widgets/plotter.tsx +0 -1194
  455. package/src/widgets/radio/__stories__/base-radio.stories.tsx +0 -196
  456. package/src/widgets/radio/__stories__/choice-icon.stories.tsx +0 -166
  457. package/src/widgets/radio/__stories__/choice-none-above.stories.tsx +0 -40
  458. package/src/widgets/radio/__stories__/choice.stories.tsx +0 -137
  459. package/src/widgets/radio/__stories__/focus-ring.stories.tsx +0 -43
  460. package/src/widgets/radio/__stories__/option-status.stories.tsx +0 -103
  461. package/src/widgets/radio/base-radio.tsx +0 -511
  462. package/src/widgets/radio/choice-a11y-text.ts +0 -61
  463. package/src/widgets/radio/choice-icon/choice-icon.tsx +0 -218
  464. package/src/widgets/radio/choice-icon/cross-out-line.tsx +0 -51
  465. package/src/widgets/radio/choice-icon/shared-styles.ts +0 -15
  466. package/src/widgets/radio/choice-none-above.tsx +0 -56
  467. package/src/widgets/radio/choice.jsx.fixture.ts +0 -45
  468. package/src/widgets/radio/choice.tsx +0 -359
  469. package/src/widgets/radio/focus-ring.tsx +0 -57
  470. package/src/widgets/radio/option-status.tsx +0 -98
  471. package/src/widgets/radio/util.test.ts +0 -18
  472. package/src/widgets/radio/util.ts +0 -31
  473. package/src/widgets/radio/widget.tsx +0 -520
  474. package/src/widgets/radio.ts +0 -157
  475. package/src/widgets/reaction-diagram.tsx +0 -144
  476. package/src/widgets/sequence.tsx +0 -142
  477. package/src/widgets/simulator.tsx +0 -829
  478. package/src/widgets/sorter.tsx +0 -155
  479. package/src/widgets/table.tsx +0 -393
  480. package/src/widgets/unit.tsx +0 -374
  481. package/src/widgets/video-transcript-link.test.tsx +0 -213
  482. package/src/widgets/video-transcript-link.tsx +0 -84
  483. package/src/widgets/video.tsx +0 -135
  484. package/src/widgets.ts +0 -489
  485. package/src/zoom.ts +0 -555
  486. package/tsconfig-build.json +0 -26
  487. package/tsconfig-build.tsbuildinfo +0 -1
@@ -1,1816 +0,0 @@
1
- import {describe, beforeAll, beforeEach, it} from "@jest/globals";
2
- import {screen, waitFor, within} from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import * as React from "react";
5
-
6
- import "@testing-library/jest-dom"; // Imports custom matchers
7
-
8
- import {clone} from "../../../../testing/object-utils";
9
- import {testDependencies} from "../../../../testing/test-dependencies";
10
- import {
11
- dropdownWidget,
12
- imageWidget,
13
- inputNumberWidget,
14
- question1,
15
- question2,
16
- mockedItem,
17
- mockedRandomItem,
18
- mockedShuffledRadioProps,
19
- } from "../__testdata__/renderer.testdata";
20
- import * as Dependencies from "../dependencies";
21
- import {Errors} from "../logging/log";
22
- import {registerWidget} from "../widgets";
23
- import {renderQuestion} from "../widgets/__tests__/renderQuestion";
24
- import InputNumberExport from "../widgets/input-number";
25
- import RadioWidgetExport from "../widgets/radio";
26
-
27
- import MockWidgetExport from "./mock-widget";
28
-
29
- import type {
30
- DropdownWidget,
31
- PerseusImageWidgetOptions,
32
- PerseusInputNumberWidgetOptions,
33
- } from "../perseus-types";
34
- import type {APIOptions} from "../types";
35
-
36
- // NOTE(jeremy): We can't use an automatic mock for the translation linter,
37
- // because one of it's "instance" methods is created using `debounce` and Jest
38
- // doesn't provide a mocked instance method for it (I suspect that Jest doesn't
39
- // see that symbol as an instance method).
40
- const mockRunLinter = jest.fn();
41
- const mockApplyLintErrors = jest.fn();
42
- jest.mock("../translation-linter", () => {
43
- // We mock the TranslationLinter constructor here setting things up so we
44
- // can spy/verify calls to instances of TranslationLinter
45
- return function () {
46
- return {
47
- runLinter: mockRunLinter,
48
- applyLintErrors: mockApplyLintErrors,
49
- };
50
- };
51
- });
52
-
53
- describe("renderer", () => {
54
- beforeAll(() => {
55
- registerWidget("input-number", InputNumberExport);
56
- registerWidget("radio", RadioWidgetExport);
57
- registerWidget("mock-widget", MockWidgetExport);
58
- });
59
-
60
- beforeEach(() => {
61
- jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
62
- testDependencies,
63
- );
64
- });
65
-
66
- afterEach(() => {
67
- // The Renderer uses a timer to wait for widgets to complete rendering.
68
- // If we don't spin the timers here, then the timer fires in the test
69
- // _after_ and breaks it because we do setState() in the callback,
70
- // and by that point the component has been unmounted.
71
- jest.runOnlyPendingTimers();
72
- });
73
-
74
- describe("snapshots", () => {
75
- it("initial render", () => {
76
- // Arrange and Act
77
- const {container} = renderQuestion(question1);
78
-
79
- // Assert
80
- expect(container).toMatchSnapshot("initial render");
81
- });
82
-
83
- it("correct answer", () => {
84
- // Arrange
85
- const {container} = renderQuestion(question1);
86
-
87
- // Act
88
- userEvent.click(screen.getByRole("button"));
89
- userEvent.click(screen.getAllByRole("option")[2]);
90
- jest.runOnlyPendingTimers();
91
-
92
- // Assert
93
- expect(container).toMatchSnapshot("correct answer");
94
- });
95
-
96
- it("incorrect answer", () => {
97
- // Arrange
98
- const {container} = renderQuestion(question1);
99
-
100
- // Act
101
- userEvent.click(screen.getByRole("button"));
102
- userEvent.click(screen.getAllByRole("option")[1]);
103
- jest.runOnlyPendingTimers();
104
-
105
- // Assert
106
- expect(container).toMatchSnapshot("incorrect answer");
107
- });
108
- });
109
-
110
- describe("linting (TranslationLinter)", () => {
111
- const extraProps = {
112
- linterContext: {
113
- contentType: "exercise",
114
- highlightLint: true,
115
- // TODO(CP-4838): is it okay to use [] as a default?
116
- paths: [],
117
- stack: [],
118
- },
119
- } as const;
120
-
121
- it("should run the linter on mount", () => {
122
- // Arrange and Act
123
- renderQuestion(question1, {}, extraProps);
124
-
125
- // Assert
126
- expect(mockRunLinter).toHaveBeenCalledWith(
127
- "The total number of boxes the forklift can carry is [[☃ dropdown 1]] $60$.",
128
- expect.any(Function),
129
- );
130
- });
131
-
132
- it("should do nothing in linter callback if component is already unmounted", () => {
133
- // Arrange
134
- const {unmount} = renderQuestion(question1, {}, extraProps);
135
- unmount();
136
- mockApplyLintErrors.mockClear();
137
-
138
- // Act - Call the linter callback
139
- // `.calls` is an array of calls to this mock.
140
- // Each call is an array of the parameters passed to that call.
141
- // So `[0][1]` is the first call's second parameter.
142
- mockRunLinter.mock.calls[0][1]([]);
143
-
144
- // Assert
145
- expect(mockApplyLintErrors).not.toHaveBeenCalled();
146
- });
147
-
148
- it("should run linter on update", () => {
149
- // Arrange
150
- const {rerender} = renderQuestion(question1, {}, extraProps);
151
-
152
- mockRunLinter.mockClear();
153
-
154
- // Act
155
- rerender(question1, {...extraProps, problemNum: 1});
156
-
157
- // Assert
158
- expect(mockRunLinter).toHaveBeenCalled();
159
- });
160
- });
161
-
162
- describe("initial render", () => {
163
- it("should build initial state from props passed in", () => {
164
- // Arrange and Act
165
- const {renderer} = renderQuestion(question1);
166
-
167
- // Assert
168
- expect(renderer.state.jiptContent).toBeNull();
169
- expect(renderer.state.translationLintErrors).toHaveLength(0);
170
- expect(renderer.state.lastUsedWidgetId).toBeNull();
171
-
172
- expect(renderer.state.widgetInfo).toStrictEqual(question1.widgets);
173
- expect(renderer.state.widgetProps).toMatchInlineSnapshot(`
174
- {
175
- "dropdown 1": {
176
- "choices": [
177
- "greater than or equal to",
178
- "less than or equal to",
179
- ],
180
- "placeholder": "greater/less than or equal to",
181
- },
182
- }
183
- `);
184
- });
185
-
186
- it("should derive type widget ID if type missing", () => {
187
- // Arrange
188
- // Note that the types disallow this, but our Renderer handles the
189
- // case so for now, I'm adding this test. We can remove the test
190
- // if/when we clean up the code for it in _getAllWidgetsInfo().
191
- const question = {
192
- ...question1,
193
- widgets: {
194
- ...question1.widgets,
195
- // $FlowIgnore[prop-missing]
196
- // $FlowIgnore[incompatible-cast]
197
- // @ts-expect-error - TS2352 - Conversion of type '{ type: undefined; static?: boolean | undefined; graded?: boolean | undefined; alignment?: string | undefined; options: PerseusCategorizerWidgetOptions | null | undefined; key?: number | undefined; version?: Version | undefined; } | ... 38 more ... | { ...; }' to type 'DropdownWidget' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
198
- "dropdown 1": {
199
- ...question1.widgets["dropdown 1"],
200
- // $FlowIgnore[incompatible-cast]
201
- type: undefined,
202
- } as DropdownWidget,
203
- },
204
- } as const;
205
-
206
- // Act
207
- const {renderer} = renderQuestion(question);
208
-
209
- // Assert
210
- expect(renderer.state.widgetInfo["dropdown 1"]?.type).toBe(
211
- "dropdown",
212
- );
213
- });
214
-
215
- it("should default alignment if missing", () => {
216
- // Arrange/Act
217
- const {renderer} = renderQuestion({
218
- ...question1,
219
- widgets: {
220
- ...question1.widgets,
221
- "dropdown 1": {
222
- ...question1.widgets["dropdown 1"],
223
- alignment: undefined,
224
- },
225
- },
226
- });
227
-
228
- // Assert
229
- expect(renderer.state.widgetInfo["dropdown 1"]?.alignment).toBe(
230
- "default",
231
- );
232
- });
233
-
234
- it("should build default widget info if widget ID missing from widget props", () => {
235
- // Arrange
236
- const {renderer} = renderQuestion({
237
- ...question1,
238
- content:
239
- "[[☃ dropdown 1]] exists.\n\n" +
240
- "[[☃ dropdown 2]] does not!",
241
- });
242
- expect(question1.widgets["dropdown 2"]).toBeUndefined();
243
- let sawDropdown2 = false;
244
-
245
- // Act
246
- const widgets = renderer.findInternalWidgets((id, info, widget) => {
247
- // We should see the Renderer dealing with `dropdown 2` even if
248
- // no widget options/props were provided in the Perseus item.
249
- if (id === "dropdown 2") {
250
- // Dropdown 2's info is the default value (ie. it's got a
251
- // type and empty options object).
252
- expect(info.type).toBe("dropdown");
253
- expect(info.options).toStrictEqual({});
254
- expect(info.graded).toBeTrue();
255
-
256
- sawDropdown2 = true;
257
- return true;
258
- }
259
- return false;
260
- });
261
-
262
- // Assert
263
- expect(sawDropdown2).toBeTrue();
264
- // `dropdown 2` not found, however, because the Renderer doesn't
265
- // render widget's that don't have options defined.
266
- expect(widgets).toStrictEqual([null]);
267
- });
268
-
269
- it("should restore serialized state on mount if provided in prop", () => {
270
- // Arrange
271
- renderQuestion(
272
- question1,
273
- {},
274
- {
275
- serializedState: {
276
- "dropdown 1": {
277
- placeholder: "greater/less than or equal to",
278
- choices: [
279
- "greater than or equal to",
280
- "less than or equal to",
281
- ],
282
- selected: 2,
283
- },
284
- },
285
- },
286
- );
287
-
288
- // Assert
289
- expect(screen.getByRole("button")).toHaveTextContent(
290
- /^less than or equal to$/,
291
- );
292
- });
293
-
294
- it("should call the onWidgetStartProps callback if provided in apiOptions", () => {
295
- // Arrange
296
- const onWidgetStartProps = jest.fn();
297
- const apiOptions: APIOptions = {onWidgetStartProps};
298
-
299
- // Act
300
- renderQuestion(mockedRandomItem, apiOptions);
301
-
302
- // Assert
303
- expect(onWidgetStartProps).toHaveBeenCalledWith(
304
- mockedShuffledRadioProps,
305
- );
306
- });
307
- });
308
-
309
- describe("rendering", () => {
310
- const images: Array<Record<any, any>> = [];
311
- let originalImage;
312
-
313
- beforeEach(() => {
314
- originalImage = window.Image;
315
- // Mock HTML Image so we can trigger onLoad callbacks and see full
316
- // image rendering.
317
- // @ts-expect-error - TS2322 - Type 'Mock<Record<string, any>, [], any>' is not assignable to type 'new (width?: number | undefined, height?: number | undefined) => HTMLImageElement'.
318
- window.Image = jest.fn(() => {
319
- const img: Record<string, any> = {};
320
- images.push(img);
321
- return img;
322
- });
323
- });
324
-
325
- afterEach(() => {
326
- window.Image = originalImage;
327
- });
328
-
329
- // Tells the image loader 1, or all, of our images loaded
330
- const markImagesAsLoaded = (imageIndex?: number) => {
331
- if (imageIndex != null) {
332
- const img = images[imageIndex];
333
- if (img?.onload) {
334
- img.onload();
335
- }
336
- } else {
337
- images.forEach((i) => {
338
- if (i?.onload) {
339
- i.onload();
340
- }
341
- });
342
- }
343
- };
344
-
345
- it.each([true, false])(
346
- "should render a table when isMobile: %s",
347
- (isMobile: boolean) => {
348
- // Arrange
349
- const question = {
350
- content:
351
- "| Heading 1 | Heading 2 | Heading 3 |\n" +
352
- "| --------- | --------- | --------- |\n" +
353
- "| r1c1 | r1c2 | r1c3 |\n" +
354
- "| r2c1 | r2c2 | r2c3 |\n" +
355
- "| r3c1 | r3c2 | r3c3 |",
356
- images: {},
357
- widgets: {},
358
- } as const;
359
-
360
- // Act
361
- const {container} = renderQuestion(question, {
362
- isMobile,
363
- });
364
-
365
- // Assert
366
- expect(container).toMatchSnapshot();
367
- },
368
- );
369
-
370
- it("should wrap image to be responsive if not in table and dimensions provided", () => {
371
- // Arrange
372
- const question = {
373
- content:
374
- "One image with dimensions that isn't in a table.\n\n" +
375
- "![This image has dimensions](https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg)",
376
- images: {
377
- "https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg":
378
- {height: 410, width: 420},
379
- },
380
- widgets: {},
381
- } as const;
382
- renderQuestion(question);
383
-
384
- // Act
385
- markImagesAsLoaded();
386
-
387
- // Assert
388
- const imageNodes = screen.queryAllByRole("img");
389
- expect(imageNodes).toHaveLength(1);
390
-
391
- // eslint-disable-next-line testing-library/no-node-access
392
- expect(imageNodes[0].parentElement).toHaveClass(
393
- "fixed-to-responsive",
394
- );
395
- });
396
-
397
- it("should not wrap image to be responsive if not in table and no dimensions provided", () => {
398
- // Arrange
399
- const question = {
400
- content:
401
- "One image without dimensions.\n\n" +
402
- "![This image doesn't have dimensions](https://ka-perseus-images.s3.amazonaws.com/41dbe22e2d8aa0eb4356195d634c3219dc7142c9.svg)",
403
- images: {},
404
- widgets: {},
405
- } as const;
406
- renderQuestion(question);
407
-
408
- // Act
409
- markImagesAsLoaded();
410
-
411
- // Assert
412
- const imageNodes = screen.queryAllByRole("img");
413
- expect(imageNodes).toHaveLength(1);
414
-
415
- // image didn't have dimensions provided so it is not rendered as
416
- // responsive.
417
- // eslint-disable-next-line testing-library/no-node-access
418
- expect(imageNodes[0].parentElement).not.toHaveClass(
419
- "fixed-to-responsive",
420
- );
421
- });
422
-
423
- it("should add dimensions to wrapper, if provided", () => {
424
- // Arrange
425
- const question = {
426
- content:
427
- "One image with dimensions.\n\n" +
428
- "![This image has dimensions](https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg)",
429
- images: {
430
- "https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg":
431
- {height: 410, width: 420},
432
- },
433
- widgets: {},
434
- } as const;
435
- renderQuestion(question);
436
-
437
- // Act
438
- markImagesAsLoaded();
439
-
440
- // Assert
441
- expect(screen.queryAllByRole("img")).toHaveLength(1);
442
-
443
- const wrapperStyle = getComputedStyle(
444
- // $FlowIgnore[incompatible-call]
445
- // @ts-expect-error - TS2345 - Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element'.
446
- screen.getByAltText("This image has dimensions").parentElement, // eslint-disable-line testing-library/no-node-access
447
- );
448
- expect(wrapperStyle.maxWidth).toBe("420px");
449
- expect(wrapperStyle.maxHeight).toBe("410px");
450
- });
451
-
452
- it("should not add dimensions to wrapper, if not provided", () => {
453
- // Arrange
454
- const question = {
455
- content:
456
- "One image without dimensions.\n\n" +
457
- "![This image doesn't](https://ka-perseus-images.s3.amazonaws.com/41dbe22e2d8aa0eb4356195d634c3219dc7142c9.svg)",
458
- images: {},
459
- widgets: {},
460
- } as const;
461
- renderQuestion(question);
462
-
463
- // Act
464
- markImagesAsLoaded();
465
-
466
- // Assert
467
- expect(screen.queryAllByRole("img")).toHaveLength(1);
468
-
469
- const wrapperStyle = getComputedStyle(
470
- // $FlowIgnore[incompatible-call]
471
- // @ts-expect-error - TS2345 - Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element'.
472
- screen.getByAltText("This image doesn't").parentElement, // eslint-disable-line testing-library/no-node-access
473
- );
474
- expect(wrapperStyle.maxWidth).toBe("");
475
- expect(wrapperStyle.maxHeight).toBe("");
476
- });
477
-
478
- it("should never render image in responsive mode if inside table", () => {
479
- // Arrange
480
- const question = {
481
- content:
482
- "A table with images:\n\n" +
483
- "\n\n" +
484
- "| Title | Image |\n" +
485
- "| ----- |------ |\n" +
486
- "| Are you hungry for pizza? |\n" +
487
- "| ![This image has dimensions](https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg) ",
488
- images: {
489
- "https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg":
490
- {height: 410, width: 420},
491
- },
492
- widgets: {},
493
- } as const;
494
- renderQuestion(question);
495
-
496
- // Act
497
- markImagesAsLoaded();
498
-
499
- // Assert
500
- const imageNodes = screen.queryAllByRole("img");
501
- expect(imageNodes).toHaveLength(1);
502
- expect(getComputedStyle(imageNodes[0]).width).toBe("420px");
503
- expect(getComputedStyle(imageNodes[0]).height).toBe("410px");
504
-
505
- // And the parent shouldn't be a wrapper element
506
- // eslint-disable-next-line testing-library/no-node-access
507
- expect(imageNodes[0].parentElement).not.toHaveClass(
508
- "fixed-to-responsive",
509
- );
510
- });
511
-
512
- it("should render widgetPlaceholder if provided", () => {
513
- // Arrange
514
- const widgetPlaceholder = <div key="1">This is a placeholder</div>;
515
-
516
- // Act
517
- renderQuestion(question1, {
518
- widgetPlaceholder,
519
- });
520
-
521
- // Assert
522
- expect(
523
- screen.getByText("This is a placeholder"),
524
- ).toBeInTheDocument();
525
- // Make sure the 'dropdown' widget wasn't rendered!
526
- expect(screen.queryAllByRole("button")).toHaveLength(0);
527
- });
528
-
529
- it("should render columns", () => {
530
- // Arrange
531
- const question = {
532
- content:
533
- "Stuff in column 1\n\n" +
534
- "=====\n\n" +
535
- "Stuff in column 2\n\n",
536
- images: {},
537
- widgets: {},
538
- } as const;
539
-
540
- // Act
541
- const {container} = renderQuestion(question);
542
-
543
- // Assert
544
- expect(container).toMatchSnapshot();
545
- });
546
-
547
- it("should render image placeholder for links found in content that are images (for translation support)", () => {
548
- // Arrange
549
- const question = {
550
- content:
551
- "A link to an image: https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg",
552
- images: {},
553
- widgets: {},
554
- } as const;
555
- const imagePlaceholder = (
556
- // Without a `key`, we get React warnings
557
- <div key="placeholder">image placeholder</div>
558
- );
559
-
560
- // Act
561
- renderQuestion(question, {
562
- imagePlaceholder,
563
- });
564
-
565
- // Assert
566
- const renderedPlaceholder = screen.getByText("image placeholder");
567
- expect(renderedPlaceholder).toBeInTheDocument();
568
- // eslint-disable-next-line testing-library/no-node-access
569
- expect(renderedPlaceholder.parentElement).toMatchInlineSnapshot(`
570
- <a
571
- href="https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg"
572
- rel="noopener noreferrer"
573
- target="_blank"
574
- >
575
- <div>
576
- image placeholder
577
- </div>
578
- </a>
579
- `);
580
- });
581
-
582
- it("should render imagePlaceholder for all images", () => {
583
- // Arrange
584
- const imagePlaceholder = (
585
- <div key="1">This is an image placeholder</div>
586
- );
587
-
588
- // Act
589
- renderQuestion(
590
- {
591
- content:
592
- "We need more cat gifs: \n\n" +
593
- "![this isn't a cat gif :(](https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg)\n\n" +
594
- "![this isn't a cat gif :(](https://ka-perseus-images.s3.amazonaws.com/41dbe22e2d8aa0eb4356195d634c3219dc7142c9.svg)",
595
- images: {
596
- "https://ka-perseus-images.s3.amazonaws.com/29e9aa32de3731b09e245e416cbf4e3fb2e89b58.svg":
597
- {height: 420, width: 420},
598
- "https://ka-perseus-images.s3.amazonaws.com/41dbe22e2d8aa0eb4356195d634c3219dc7142c9.svg":
599
- {height: 216, width: 216},
600
- },
601
- widgets: {},
602
- },
603
- {
604
- imagePlaceholder,
605
- },
606
- );
607
-
608
- // Assert
609
- expect(
610
- screen.getAllByText("This is an image placeholder"),
611
- ).toHaveLength(2);
612
- });
613
-
614
- it("should render warning if same widget included multiple times in content", () => {
615
- // Arrange
616
- const question = {
617
- ...question1,
618
- content: question1.content + "\n\n[[☃ dropdown 1]]",
619
- } as const;
620
-
621
- // Act
622
- renderQuestion(question);
623
-
624
- // Assert
625
- expect(
626
- screen.getByText("Widget [[☃ dropdown 1]] already exists."),
627
- ).toBeInTheDocument();
628
- });
629
-
630
- it("should render block math", async () => {
631
- // Arrange
632
- const question = {
633
- content:
634
- "This is some block math\n\n" +
635
- "$1 + 2$\n\n" +
636
- "And a footer.",
637
- images: {},
638
- widgets: {},
639
- } as const;
640
- const apiOptions: Record<string, any> = {};
641
-
642
- // Act
643
- const {container} = renderQuestion(question, apiOptions);
644
- // KaTeX renders async, so we have to wait until it's done.
645
- await waitFor(() => {
646
- screen.getByText("1 + 2");
647
- });
648
-
649
- // Assert
650
- expect(container).toMatchSnapshot();
651
- });
652
-
653
- it("should render block math on mobile", async () => {
654
- // Arrange
655
- const question = {
656
- content:
657
- "This is some block math\n\n" +
658
- "$1 + 2$\n\n" +
659
- "And a footer.",
660
- images: {},
661
- widgets: {},
662
- } as const;
663
- const apiOptions = {isMobile: true} as const;
664
-
665
- // Act
666
- const {container} = renderQuestion(question, apiOptions);
667
- // KaTeX renders async, so we have to wait until it's done.
668
- await waitFor(() => {
669
- screen.getByText("1 + 2");
670
- });
671
-
672
- // Assert
673
- expect(container).toMatchSnapshot();
674
- });
675
-
676
- it("should render block math in a Zoomable when on mobile", async () => {
677
- // Arrange
678
- const question = {
679
- content:
680
- "This is some block math\n\n" +
681
- "$1 + 2$\n\n" +
682
- "And a footer.",
683
- images: {},
684
- widgets: {},
685
- } as const;
686
- const apiOptions = {isMobile: true} as const;
687
-
688
- // Act
689
- const {container} = renderQuestion(question, apiOptions);
690
- // KaTeX renders async, so we have to wait until it's done.
691
- await waitFor(() => {
692
- screen.getByText("1 + 2");
693
- });
694
-
695
- // Assert
696
- // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
697
- const [zoomable] = container.querySelectorAll(
698
- ".perseus-block-math-inner span",
699
- );
700
- expect(zoomable).not.toBeUndefined();
701
- expect(zoomable).not.toBeNull();
702
- // @ts-expect-error - TS2339 - Property 'style' does not exist on type 'Element'.
703
- expect(zoomable.style.transform).not.toBe("");
704
-
705
- /* eslint-disable testing-library/no-node-access */
706
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
707
- expect(zoomable.parentElement.style.transform).not.toBe("");
708
- /* eslint-enable testing-library/no-node-access */
709
- });
710
-
711
- it("should render the math", async () => {
712
- // Arrange
713
- const question = {
714
- content: "This is some inline math: $1 + 2$",
715
- images: {},
716
- widgets: {},
717
- } as const;
718
-
719
- // Act
720
- const {container} = renderQuestion(question);
721
-
722
- // Assert
723
- await waitFor(() => {
724
- screen.getByText("1 + 2");
725
- });
726
- expect(container).toMatchSnapshot();
727
- });
728
-
729
- it("should replace deprecated alignment tags in inline math", async () => {
730
- // Arrange
731
- const question = {
732
- content:
733
- "$\\begin{align}\n2\\text{HCl}(\\text{aq})+\\text{Ca}(\\text{OH})_2(\\text{aq})\\rightarrow\\text{Ca}(\\text{s})+2\\text H_2\\text O(\\text l)+\\text{Cl}_2(\\text g)\n\\end{align}$",
734
- images: {},
735
- widgets: {},
736
- } as const;
737
-
738
- // Act
739
- renderQuestion(question);
740
-
741
- // Assert
742
- // KaTeX renders async, so we use waitFor() have to wait until it's done.
743
- await waitFor(() => {
744
- screen.getByText(/\\begin\{aligned\}.*\\end\{aligned\}/);
745
- });
746
- });
747
- });
748
-
749
- // Note that we can't use `.each` here because the three props require
750
- // slightly different methods to change them for our rerender() function
751
- describe("resets widgetInfo and widgetProps when a prop in SHOULD_CLEAR_WIDGETS_PROP_LIST changes", () => {
752
- let originalWidgetInfo;
753
- let originalWidgetProps;
754
- let rerender;
755
- let renderer;
756
-
757
- beforeEach(() => {
758
- // Arrange
759
- // eslint-disable-next-line testing-library/no-render-in-setup
760
- const result = renderQuestion(question1);
761
- rerender = result.rerender;
762
- renderer = result.renderer;
763
-
764
- originalWidgetInfo = clone(renderer.state.widgetInfo);
765
- originalWidgetProps = clone(renderer.state.widgetProps);
766
-
767
- // Poke the renderer so it's not in it's initial-render state
768
- userEvent.click(screen.getByRole("button"));
769
- jest.runOnlyPendingTimers(); // There's a setTimeout to open the dropdown
770
- userEvent.click(screen.getAllByRole("option")[1]);
771
- });
772
-
773
- it("'content' prop'", () => {
774
- // Act
775
- rerender({
776
- ...question1,
777
- content:
778
- "this is a content change but still retains the widget it had: [[☃ dropdown 1]]",
779
- });
780
-
781
- // Assert
782
- expect(renderer.state.widgetInfo).toStrictEqual(originalWidgetInfo);
783
- expect(renderer.state.widgetProps).toStrictEqual(
784
- originalWidgetProps,
785
- );
786
- });
787
-
788
- it("'problemNum' prop", () => {
789
- // Act
790
- rerender(question1, {problemNum: 1});
791
-
792
- // Assert
793
- expect(renderer.state.widgetInfo).toStrictEqual(originalWidgetInfo);
794
- expect(renderer.state.widgetProps).toStrictEqual(
795
- originalWidgetProps,
796
- );
797
- });
798
-
799
- it("'widgets' prop", () => {
800
- // Act
801
- rerender({
802
- ...question1,
803
- widgets: {
804
- ...question1.widgets,
805
- // This is just a copy of the original widget, but it
806
- // causes a 'widgets' prop change
807
- "another-widget 1": question1.widgets["dropdown 1"],
808
- },
809
- });
810
-
811
- // Assert
812
- const allWidgetIds = Object.getOwnPropertyNames(originalWidgetInfo);
813
-
814
- // Note that we are only caring about equality of keys that existed
815
- // in the original state. Of course, since we made changes to the
816
- // `widgets` prop, the two widget-dependant state fields will be
817
- // changed!
818
- for (const widgetId of allWidgetIds) {
819
- expect(renderer.state.widgetInfo[widgetId]).toStrictEqual(
820
- originalWidgetInfo[widgetId],
821
- );
822
- }
823
-
824
- for (const widgetId of allWidgetIds) {
825
- expect(renderer.state.widgetProps[widgetId]).toStrictEqual(
826
- originalWidgetProps[widgetId],
827
- );
828
- }
829
- });
830
- });
831
-
832
- describe("focus management", () => {
833
- it("should focus the first focusable widget", () => {
834
- // Arrange
835
- const question = {
836
- content:
837
- "A dropdown [[☃ dropdown 1]]\nAn input [[☃ input-number 1]]\n\nAnd an image [[☃ image 1]].",
838
- images: {},
839
- widgets: {
840
- "dropdown 1": dropdownWidget,
841
- "input-number 1": inputNumberWidget,
842
- "image 1": imageWidget,
843
- },
844
- } as const;
845
- const onFocusChange = jest.fn();
846
- const {renderer} = renderQuestion(question, {
847
- onFocusChange,
848
- });
849
-
850
- // Act
851
- const focusResult = renderer.focus();
852
-
853
- // Assert
854
- expect(focusResult).toBeTrue();
855
- expect(onFocusChange).toHaveBeenCalledWith(["dropdown 1"], null);
856
- });
857
-
858
- it("should return false if no widgets are focusable", () => {
859
- // Arrange
860
- const onFocusChange = jest.fn();
861
- const {renderer} = renderQuestion(
862
- {
863
- content: "[[☃ image 1]]",
864
- images: {},
865
- widgets: {
866
- "image 1": {
867
- alignment: "block",
868
- graded: true,
869
- options: {
870
- alt: "A number line labeled 200 to 300 with tick marks at every 5 units. The tick marks at 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, and 300 are labeled. A red circle labeled A is between 220 tick mark and 230 tick mark.",
871
- backgroundImage: {
872
- height: 80,
873
- url: "web+graphie://ka-perseus-graphie.s3.amazonaws.com/3351ccf19e60c28a1d08664f5c16defa76ed0348",
874
- width: 380,
875
- },
876
- } as PerseusImageWidgetOptions,
877
- static: false,
878
- type: "image",
879
- version: {major: 0, minor: 0},
880
- },
881
- },
882
- },
883
- {
884
- onFocusChange,
885
- },
886
- );
887
-
888
- // Act
889
- const focusResult = renderer.focus();
890
-
891
- // Assert
892
- expect(focusResult).toBeFalsy();
893
- expect(onFocusChange).not.toHaveBeenCalled();
894
- });
895
-
896
- it("should call onFocusChange callback when new widget receives focus", () => {
897
- // Arrange
898
- const onFocusChange = jest.fn();
899
- renderQuestion(
900
- {
901
- ...question2,
902
- content:
903
- "Enter 1 in this field: [[☃ input-number 1]].\n\n" +
904
- "Enter 2 in this field: [[☃ input-number 2]] $60$.",
905
- widgets: {
906
- "input-number 1": question2.widgets["input-number 1"],
907
- "input-number 2": question2.widgets["input-number 1"],
908
- },
909
- },
910
- {onFocusChange},
911
- );
912
-
913
- // Act
914
- userEvent.click(screen.getAllByRole("textbox")[1]);
915
-
916
- // Assert
917
- expect(onFocusChange).toHaveBeenCalledWith(
918
- /* new focus path */ ["input-number 2"],
919
- /* old focus path */ null,
920
- );
921
- });
922
-
923
- it("should call onFocusChange callback when a widget loses focus", () => {
924
- // Arrange
925
- const onFocusChange = jest.fn();
926
- renderQuestion(
927
- {
928
- ...question2,
929
- content:
930
- "Enter 1 in this field: [[☃ input-number 1]].\n\n" +
931
- "Enter 2 in this field: [[☃ input-number 2]] $60$.",
932
- widgets: {
933
- "input-number 1": question2.widgets["input-number 1"],
934
- "input-number 2": question2.widgets["input-number 1"],
935
- },
936
- },
937
- {onFocusChange},
938
- );
939
- userEvent.click(screen.getAllByRole("textbox")[1]);
940
- onFocusChange.mockClear();
941
-
942
- // Act
943
- userEvent.tab();
944
- // There's a _.defer() in the blur handling
945
- jest.runOnlyPendingTimers();
946
-
947
- // Assert
948
- expect(onFocusChange).toHaveBeenCalledWith(
949
- /* new focus path */ null,
950
- /* old focus path */ ["input-number 2"],
951
- );
952
- });
953
-
954
- it("should throw if widget provides invalid focus path", () => {
955
- // Arrange
956
- const {renderer} = renderQuestion(mockedItem);
957
- const [widget2] = renderer.findWidgets("mock-widget 2");
958
-
959
- // Act and Assert
960
- expect(() => {
961
- // TODO: Right now KAError is mocked because we're
962
- // jest.mock()ing the logging module. Figure out how to
963
- // unmock.
964
- widget2.props.onFocus("this is not an array");
965
- }).toThrow("widget props.onFocus focusPath must be an Array");
966
- });
967
-
968
- it("should focus the input at the requested FocusPath", () => {
969
- // Arrange
970
- const {renderer} = renderQuestion(question2);
971
-
972
- // Act
973
- renderer.focusPath(["input-number 1"]);
974
-
975
- // Assert
976
- expect(screen.getByRole("textbox")).toHaveFocus();
977
- });
978
-
979
- it("should do nothing if requested FocusPath is already focused", () => {
980
- // Arrange
981
- const onFocusChange = jest.fn();
982
- const {renderer} = renderQuestion(question2, {
983
- onFocusChange,
984
- });
985
- renderer.focusPath(["input-number 1"]);
986
- onFocusChange.mockClear();
987
-
988
- // Act
989
- renderer.focusPath(["input-number 1"]);
990
-
991
- // Assert
992
- expect(onFocusChange).not.toHaveBeenCalled();
993
- });
994
-
995
- it("should blur current widget when focus changes", () => {
996
- // Arrange
997
- const onFocusChange = jest.fn();
998
- const {renderer} = renderQuestion(
999
- {
1000
- ...question2,
1001
- content:
1002
- "Input 1: [[☃ input-number 1]]\n\n" +
1003
- "Input 2: [[☃ input-number 2]]",
1004
- widgets: {
1005
- ...question2.widgets,
1006
- "input-number 2": question2.widgets["input-number 1"],
1007
- },
1008
- },
1009
- {onFocusChange},
1010
- );
1011
- renderer.focusPath(["input-number 1"]);
1012
- onFocusChange.mockClear();
1013
-
1014
- // Act
1015
- renderer.focusPath(["input-number 2"]);
1016
-
1017
- // Assert
1018
- expect(onFocusChange).toHaveBeenCalledWith(
1019
- ["input-number 2"], // New focus
1020
- ["input-number 1"], // Old focus
1021
- );
1022
- });
1023
-
1024
- it("should do nothing if blurred widget is not focused", () => {
1025
- // Arrange
1026
- const onFocusChange = jest.fn();
1027
- const {renderer} = renderQuestion(
1028
- {
1029
- ...question2,
1030
- content:
1031
- "Input 1: [[☃ input-number 1]]\n\n" +
1032
- "Input 2: [[☃ input-number 2]]",
1033
- widgets: {
1034
- ...question2.widgets,
1035
- "input-number 2": question2.widgets["input-number 1"],
1036
- },
1037
- },
1038
- {onFocusChange},
1039
- );
1040
- // Focus _second_ input number widget
1041
- screen.getAllByRole("textbox")[1].focus();
1042
- onFocusChange.mockClear();
1043
-
1044
- // Act
1045
- renderer.blurPath(["input-number 1"]);
1046
-
1047
- // Assert
1048
- expect(onFocusChange).not.toHaveBeenCalled();
1049
- });
1050
-
1051
- it("should blur current widget", () => {
1052
- // Arrange
1053
- const onFocusChange = jest.fn();
1054
- const {renderer} = renderQuestion(
1055
- {
1056
- ...question2,
1057
- content:
1058
- "Input 1: [[☃ input-number 1]]\n\n" +
1059
- "Input 2: [[☃ input-number 2]]",
1060
- widgets: {
1061
- ...question2.widgets,
1062
- "input-number 2": question2.widgets["input-number 1"],
1063
- },
1064
- },
1065
- {onFocusChange},
1066
- );
1067
- // Focus _second_ input number widget
1068
- screen.getAllByRole("textbox")[1].focus();
1069
- onFocusChange.mockClear();
1070
-
1071
- // Act
1072
- renderer.blur();
1073
- jest.runOnlyPendingTimers(); // There's a _.defer() in this code path
1074
-
1075
- // Assert
1076
- expect(onFocusChange).toHaveBeenCalledWith(
1077
- null, // New focus
1078
- ["input-number 2"], // Old focus
1079
- );
1080
- });
1081
-
1082
- it("should do nothing when blur() called but no widget focused", () => {
1083
- // Arrange
1084
- const onFocusChange = jest.fn();
1085
- const {renderer} = renderQuestion(
1086
- {
1087
- ...question2,
1088
- content:
1089
- "Input 1: [[☃ input-number 1]]\n\n" +
1090
- "Input 2: [[☃ input-number 2]]",
1091
- widgets: {
1092
- ...question2.widgets,
1093
- "input-number 2": question2.widgets["input-number 1"],
1094
- },
1095
- },
1096
- {onFocusChange},
1097
- );
1098
-
1099
- // Act
1100
- renderer.blur();
1101
- jest.runOnlyPendingTimers(); // There's a _.defer() in this code path
1102
-
1103
- // Assert
1104
- expect(onFocusChange).not.toHaveBeenCalled();
1105
- });
1106
- });
1107
-
1108
- describe("state serialization", () => {
1109
- it("should request widget's serialized state if implemented", () => {
1110
- // Arrange
1111
- const {renderer} = renderQuestion(mockedItem);
1112
-
1113
- const [widget2] = renderer.findWidgets("mock-widget 2");
1114
- expect(widget2).not.toBeUndefined();
1115
- widget2.getSerializedState = jest.fn();
1116
-
1117
- // Act
1118
- renderer.getSerializedState();
1119
-
1120
- // Act
1121
- expect(widget2.getSerializedState).toHaveBeenCalled();
1122
- });
1123
-
1124
- it("should skip restoration if state's widget ID list doesn't match renderer widgets", () => {
1125
- // Arrange
1126
- const errorSpy = jest.spyOn(testDependencies.Log, "error");
1127
-
1128
- const {renderer} = renderQuestion(question1);
1129
-
1130
- // Act
1131
- renderer.restoreSerializedState({
1132
- "group 1": {},
1133
- "interactive-chart 1": {},
1134
- });
1135
-
1136
- // Assert
1137
- expect(errorSpy).toHaveBeenCalledWith(
1138
- "Refusing to restore bad serialized state:",
1139
- Errors.Internal,
1140
- {
1141
- loggedMetadata: {
1142
- currentProps: expect.anything(),
1143
- serializedState:
1144
- '{"group 1":{},"interactive-chart 1":{}}',
1145
- },
1146
- },
1147
- );
1148
- });
1149
-
1150
- it("should fire restoration callback when all widgets have restored", () => {
1151
- // Arrange
1152
- const makeRestoreSerializedStateMock = jest
1153
- .fn()
1154
- .mockImplementation((props, callback) => callback());
1155
-
1156
- const {renderer} = renderQuestion(mockedItem);
1157
- const [widget1, widget2, widget3] = renderer.findWidgets(
1158
- // @ts-expect-error - TS2367 - This condition will always return 'false' since the types '"video" | "image" | "iframe" | "table" | "radio" | "definition" | "group" | "matrix" | "categorizer" | "cs-program" | "dropdown" | "example-graphie-widget" | "example-widget" | ... 26 more ... | "unit-input"' and '"mock-widget"' have no overlap.
1159
- (_, info) => info.type === "mock-widget",
1160
- );
1161
- widget1.restoreSerializedState = makeRestoreSerializedStateMock;
1162
- widget2.restoreSerializedState = makeRestoreSerializedStateMock;
1163
- widget3.restoreSerializedState = makeRestoreSerializedStateMock;
1164
-
1165
- const restorationCallback = jest.fn();
1166
-
1167
- // Act
1168
- renderer.restoreSerializedState(
1169
- {"mock-widget 1": {}, "mock-widget 2": {}, "mock-widget 3": {}},
1170
- restorationCallback,
1171
- );
1172
- jest.runOnlyPendingTimers();
1173
-
1174
- // Assert
1175
- expect(restorationCallback).toHaveBeenCalledTimes(1);
1176
- });
1177
-
1178
- it("should return each widget's state from serialize()", () => {
1179
- // Arrange
1180
- const {renderer} = renderQuestion(mockedItem);
1181
- const widgets = renderer.findWidgets((id) =>
1182
- ["mock-widget 1", "mock-widget 2", "mock-widget 3"].includes(
1183
- id,
1184
- ),
1185
- );
1186
- widgets.forEach((w) => {
1187
- w.serialize = jest.fn(() => `State: ${w.props.widgetId}`);
1188
- });
1189
- // It takes a clock tick after rendering for widgetInfo to be
1190
- // populated (which renderer uses during serialize()).
1191
- jest.runOnlyPendingTimers();
1192
-
1193
- // Act
1194
- const state = renderer.serialize();
1195
-
1196
- // Assert
1197
- expect(state).toStrictEqual({
1198
- "mock-widget 1": "State: mock-widget 1",
1199
- "mock-widget 2": "State: mock-widget 2",
1200
- "mock-widget 3": "State: mock-widget 3",
1201
- });
1202
- });
1203
- });
1204
-
1205
- describe("finding widgets", () => {
1206
- it("should be able to find widgets by widget ID string", () => {
1207
- // Arrange
1208
- const {renderer} = renderQuestion(question1);
1209
-
1210
- // Act
1211
- const [dropdown1] = renderer.findWidgets("dropdown 1");
1212
-
1213
- // Assert
1214
- expect(dropdown1).not.toBeNull();
1215
- expect(dropdown1).not.toBeUndefined();
1216
- });
1217
-
1218
- it("should be able to find widgets by widget type string", () => {
1219
- // Arrange
1220
- const {renderer} = renderQuestion(question1);
1221
-
1222
- // Act
1223
- const [dropdown1] = renderer.findWidgets("dropdown");
1224
-
1225
- // Assert
1226
- expect(dropdown1).not.toBeNull();
1227
- expect(dropdown1).not.toBeUndefined();
1228
- });
1229
-
1230
- it("should be able to find widgets by filter function", () => {
1231
- // Arrange
1232
- const {renderer} = renderQuestion(question1);
1233
- let filterCalled = false;
1234
-
1235
- // Act and Arrange
1236
- renderer.findWidgets((id, info, widget) => {
1237
- filterCalled = true;
1238
- expect(id).toBe("dropdown 1");
1239
- expect(info.type).toBe("dropdown");
1240
- expect(widget).not.toBeNull();
1241
- expect(widget).not.toBeUndefined();
1242
- return false;
1243
- });
1244
-
1245
- expect(filterCalled).toBeTrue();
1246
- });
1247
- });
1248
-
1249
- describe("misc behaviors", () => {
1250
- it("should be able to force re-render", () => {
1251
- // Arrange
1252
- const onRender = jest.fn();
1253
- const apiOptions: Record<string, any> = {};
1254
- const extraProps = {
1255
- alwaysUpdate: true, // Switching to `false` to fails test
1256
- onRender,
1257
- } as const;
1258
- const {rerender} = renderQuestion(
1259
- question1,
1260
- apiOptions,
1261
- extraProps,
1262
- );
1263
-
1264
- // Act
1265
- rerender(question1, extraProps);
1266
-
1267
- // How do I tell it rerendered?
1268
- expect(onRender).toHaveBeenCalledTimes(2);
1269
- });
1270
-
1271
- it("should use new serializedState if getSerializedState is different", () => {
1272
- // Act
1273
- // Render with serialized state
1274
- const {rerender} = renderQuestion(
1275
- question1,
1276
- {},
1277
- {
1278
- serializedState: {
1279
- "dropdown 1": {
1280
- placeholder: "greater/less than or equal to",
1281
- choices: [
1282
- "greater than or equal to",
1283
- "less than or equal to",
1284
- ],
1285
- selected: 2, // <-- Important
1286
- },
1287
- },
1288
- },
1289
- );
1290
-
1291
- // Act
1292
- rerender(question1, {
1293
- serializedState: {
1294
- "dropdown 1": {
1295
- placeholder: "greater/less than or equal to",
1296
- choices: [
1297
- "greater than or equal to",
1298
- "less than or equal to",
1299
- ],
1300
- selected: 1, // <-- Important
1301
- },
1302
- },
1303
- });
1304
-
1305
- expect(screen.getByRole("button")).toHaveTextContent(
1306
- /greater than or equal to/,
1307
- );
1308
- });
1309
-
1310
- it("should render the widget in full width if alignment == 'fullWidth'", () => {
1311
- // Arrange/Act
1312
- renderQuestion({
1313
- ...question1,
1314
- widgets: {
1315
- ...question1.widgets,
1316
- "dropdown 1": {
1317
- ...question1.widgets["dropdown 1"],
1318
- alignment: "full-width",
1319
- },
1320
- },
1321
- });
1322
-
1323
- // Assert
1324
- let el = screen.getByRole("button");
1325
- while (el != null) {
1326
- if (el.classList.contains("widget-full-width")) {
1327
- break;
1328
- }
1329
- // eslint-disable-next-line testing-library/no-node-access
1330
- // @ts-expect-error - TS2322 - Type 'HTMLElement | null' is not assignable to type 'HTMLElement'.
1331
- // eslint-disable-next-line testing-library/no-node-access
1332
- el = el.parentElement;
1333
- }
1334
-
1335
- // Didn't find parent element that's set to full-widget
1336
- expect(el).not.toBeNull();
1337
- });
1338
-
1339
- it("should force the widget to be non-static if it has a problem number", () => {
1340
- // Arrange/Act
1341
- const {renderer} = renderQuestion(
1342
- {
1343
- ...question1,
1344
- widgets: {
1345
- ...question1.widgets,
1346
- "dropdown 1": {
1347
- ...question1.widgets["dropdown 1"],
1348
- static: true,
1349
- },
1350
- },
1351
- },
1352
- {},
1353
- {problemNum: 1},
1354
- );
1355
-
1356
- // Assert
1357
- const [dropdownWidget] = renderer.findWidgets("dropdown 1");
1358
-
1359
- // Act
1360
- expect(dropdownWidget.props.static).toBeFalse();
1361
- });
1362
-
1363
- it("should ask each widget to show rationales", () => {
1364
- // Arrange
1365
- const {renderer} = renderQuestion(mockedItem);
1366
- const widgets = renderer.findWidgets(
1367
- // @ts-expect-error - TS2367 - This condition will always return 'false' since the types '"video" | "image" | "iframe" | "table" | "radio" | "definition" | "group" | "matrix" | "categorizer" | "cs-program" | "dropdown" | "example-graphie-widget" | "example-widget" | ... 26 more ... | "unit-input"' and '"mock-widget"' have no overlap.
1368
- (_, info) => info.type === "mock-widget",
1369
- );
1370
- widgets.forEach(
1371
- (w) =>
1372
- (w.showRationalesForCurrentlySelectedChoices = jest.fn()),
1373
- );
1374
-
1375
- // Guard: If our test data were to cause this to return an empty
1376
- // array, the rest of the assertions in this test would pass.
1377
- expect(widgets.length).toBeGreaterThan(0);
1378
-
1379
- // Act
1380
- renderer.showRationalesForCurrentlySelectedChoices();
1381
-
1382
- // Assert
1383
- widgets.forEach((w) =>
1384
- expect(
1385
- w.showRationalesForCurrentlySelectedChoices,
1386
- ).toHaveBeenCalled(),
1387
- );
1388
- });
1389
-
1390
- it("should ask each widget to deselect incorrect choices", () => {
1391
- // Arrange
1392
- const {renderer} = renderQuestion(mockedItem);
1393
- const widgets = renderer.findWidgets(
1394
- // @ts-expect-error - TS2367 - This condition will always return 'false' since the types '"video" | "image" | "iframe" | "table" | "radio" | "definition" | "group" | "matrix" | "categorizer" | "cs-program" | "dropdown" | "example-graphie-widget" | "example-widget" | ... 26 more ... | "unit-input"' and '"mock-widget"' have no overlap.
1395
- (_, info) => info.type === "mock-widget",
1396
- );
1397
- widgets.forEach(
1398
- (w) => (w.deselectIncorrectSelectedChoices = jest.fn()),
1399
- );
1400
-
1401
- // Guard: If our test data were to cause this to return an empty
1402
- // array, the rest of the assertions in this test would pass.
1403
- expect(widgets.length).toBeGreaterThan(0);
1404
-
1405
- // Act
1406
- renderer.deselectIncorrectSelectedChoices();
1407
-
1408
- // Assert
1409
- widgets.forEach((w) =>
1410
- expect(w.deselectIncorrectSelectedChoices).toHaveBeenCalled(),
1411
- );
1412
- });
1413
-
1414
- it("should return user input", () => {
1415
- // Arrange
1416
- const {renderer} = renderQuestion({
1417
- ...question2,
1418
- content:
1419
- "Input 1: [[☃ input-number 1]]\n\n" +
1420
- "Input 2: [[☃ input-number 2]]\n\n" +
1421
- "A widget that doesn't implement getUserInput: [[☃ image 1]]",
1422
- widgets: {
1423
- ...question2.widgets,
1424
- "input-number 2": {
1425
- ...question2.widgets["input-number 1"],
1426
- static: true,
1427
- },
1428
- "image 1": {
1429
- type: "image",
1430
- graded: false,
1431
- options: {
1432
- backgroundImage: {
1433
- url: "https://example.com/cat.png",
1434
- width: 100,
1435
- height: 100,
1436
- },
1437
- },
1438
- },
1439
- },
1440
- });
1441
-
1442
- screen
1443
- .getAllByRole("textbox")
1444
- .forEach((el, i) => userEvent.type(el, i.toString()));
1445
-
1446
- // Act
1447
- const input = renderer.getUserInput();
1448
-
1449
- // Assert
1450
- expect(input).toStrictEqual([
1451
- {currentValue: "0"},
1452
- {currentValue: "1"},
1453
- null, // image widget doesn't implement getUserinput
1454
- ]);
1455
- });
1456
-
1457
- it("should return all widget IDs that were rendererd", () => {
1458
- // Arrange
1459
- const {renderer} = renderQuestion({
1460
- ...question2,
1461
- content:
1462
- "Input 1: [[☃ input-number 1]]\n\n" +
1463
- "Input 2: [[☃ input-number 2]]\n\n" +
1464
- "A widget that doesn't implement getUserInput: [[☃ image 1]]",
1465
- widgets: {
1466
- ...question2.widgets,
1467
- "input-number 2": {
1468
- ...question2.widgets["input-number 1"],
1469
- static: true,
1470
- },
1471
- "image 1": {
1472
- type: "image",
1473
- graded: false,
1474
- options: {
1475
- backgroundImage: {
1476
- url: "https://example.com/cat.png",
1477
- width: 100,
1478
- height: 100,
1479
- },
1480
- },
1481
- },
1482
- },
1483
- });
1484
-
1485
- // Act
1486
- const widgetIds = renderer.getWidgetIds();
1487
-
1488
- // Assert
1489
- expect(widgetIds).toStrictEqual([
1490
- "input-number 1",
1491
- "input-number 2",
1492
- "image 1",
1493
- ]);
1494
- });
1495
- });
1496
-
1497
- describe("getDOMNodeForPath", () => {
1498
- it("should return the DOM node for the widget at requested FocusPath", () => {
1499
- // Arrange
1500
- const {renderer} = renderQuestion(question1);
1501
-
1502
- // Act
1503
- const node = renderer.getDOMNodeForPath(["dropdown 1"]);
1504
-
1505
- // Assert
1506
- // "button" role is the WB dropdown's "opener" element
1507
- // @ts-expect-error - TS2345 - Argument of type 'Element | Text | null | undefined' is not assignable to parameter of type 'HTMLElement'.
1508
- expect(within(node).queryAllByRole("button")).toHaveLength(1);
1509
- });
1510
-
1511
- it("should return the widget's getDOMNodeForPath() result for the widget at requested FocusPath", () => {
1512
- // Arrange
1513
- const {renderer} = renderQuestion(mockedItem);
1514
- const widget2DOMNode = <span />;
1515
- const [widget2] = renderer.findWidgets("mock-widget 2");
1516
- widget2.getDOMNodeForPath = jest.fn(() => widget2DOMNode);
1517
-
1518
- // Act
1519
- const node = renderer.getDOMNodeForPath(["mock-widget 2"]);
1520
-
1521
- // Assert
1522
- expect(node).toBe(widget2DOMNode);
1523
- });
1524
- });
1525
-
1526
- describe("getGrammarTypeForPath", () => {
1527
- it("should return undefined if matching widget doesn't implement getGrammarTypeForPath", () => {
1528
- // Arrange
1529
- const {renderer} = renderQuestion(question1);
1530
-
1531
- // Act
1532
- const grammarType = renderer.getGrammarTypeForPath(["dropdown 1"]);
1533
-
1534
- // Assert
1535
- expect(grammarType).toBeUndefined();
1536
- });
1537
-
1538
- it("should return widget result if matching widget implements getGrammarTypeForPath", () => {
1539
- // Arrange
1540
- const {renderer} = renderQuestion(question2);
1541
-
1542
- // Act
1543
- const grammarType = renderer.getGrammarTypeForPath([
1544
- "input-number 1",
1545
- ]);
1546
-
1547
- // Assert
1548
- expect(grammarType).toBe("number");
1549
- });
1550
- });
1551
-
1552
- describe("getInputPaths", () => {
1553
- it("should return all input paths for all rendererd widgets", () => {
1554
- // Arrange
1555
- const {renderer} = renderQuestion(mockedItem);
1556
- const [mockWidget1, mockWidget2, mockWidget3] =
1557
- renderer.findWidgets((id) =>
1558
- [
1559
- "mock-widget 1",
1560
- "mock-widget 2",
1561
- "mock-widget 3",
1562
- ].includes(id),
1563
- );
1564
- mockWidget1.getInputPaths = jest.fn(() => ["input 1"]);
1565
- mockWidget2.getInputPaths = jest.fn(() => ["input 2", "input 3"]);
1566
- mockWidget3.getInputPaths = jest.fn(() => [
1567
- ["input 4", "sub-input 4.1"],
1568
- "input 5",
1569
- ]);
1570
-
1571
- // Act
1572
- const inputPaths = renderer.getInputPaths();
1573
-
1574
- // Assert
1575
- expect(inputPaths).toMatchInlineSnapshot(`
1576
- [
1577
- [
1578
- "mock-widget 1",
1579
- "input 1",
1580
- ],
1581
- [
1582
- "mock-widget 2",
1583
- "input 2",
1584
- ],
1585
- [
1586
- "mock-widget 2",
1587
- "input 3",
1588
- ],
1589
- [
1590
- "mock-widget 3",
1591
- "input 4",
1592
- "sub-input 4.1",
1593
- ],
1594
- [
1595
- "mock-widget 3",
1596
- "input 5",
1597
- ],
1598
- ]
1599
- `);
1600
- });
1601
- });
1602
-
1603
- describe("emptyWidgets", () => {
1604
- it("should return all empty widgets", () => {
1605
- // Arrange
1606
- const {renderer} = renderQuestion({
1607
- ...question2,
1608
- content:
1609
- "Input 1: [[☃ input-number 1]]\n\n" +
1610
- "Input 2: [[☃ input-number 2]]",
1611
- widgets: {
1612
- ...question2.widgets,
1613
- "input-number 2": question2.widgets["input-number 1"],
1614
- },
1615
- });
1616
- userEvent.type(screen.getAllByRole("textbox")[0], "150");
1617
-
1618
- // Act
1619
- const emptyWidgets = renderer.emptyWidgets();
1620
-
1621
- // Assert
1622
- expect(emptyWidgets).toStrictEqual(["input-number 2"]);
1623
- });
1624
-
1625
- it("should not return static widgets even if empty", () => {
1626
- // Arrange
1627
- const {renderer} = renderQuestion({
1628
- ...question2,
1629
- content:
1630
- "Input 1: [[☃ input-number 1]]\n\n" +
1631
- "Input 2: [[☃ input-number 2]]",
1632
- widgets: {
1633
- ...question2.widgets,
1634
- "input-number 2": {
1635
- ...question2.widgets["input-number 1"],
1636
- static: true,
1637
- },
1638
- },
1639
- });
1640
-
1641
- // Act
1642
- const emptyWidgets = renderer.emptyWidgets();
1643
-
1644
- // Assert
1645
- expect(emptyWidgets).toStrictEqual(["input-number 1"]);
1646
- });
1647
- });
1648
-
1649
- describe("setInputValue", () => {
1650
- it("should set the value on the requested FocusPath", () => {
1651
- // Arrange
1652
- const {renderer} = renderQuestion({
1653
- ...question2,
1654
- content:
1655
- "Input 1: [[☃ input-number 1]]\n\n" +
1656
- "Input 2: [[☃ input-number 2]]",
1657
- widgets: {
1658
- ...question2.widgets,
1659
- "input-number 2": {
1660
- ...question2.widgets["input-number 1"],
1661
- static: true,
1662
- },
1663
- },
1664
- });
1665
- const cb = jest.fn();
1666
-
1667
- // Act
1668
- renderer.setInputValue(["input-number 2"], "1000", cb);
1669
-
1670
- // Assert
1671
- expect(screen.getAllByRole("textbox")[0]).toHaveValue("");
1672
- expect(screen.getAllByRole("textbox")[1]).toHaveValue("1000");
1673
- });
1674
-
1675
- it("should call the focus callback when value is complete", () => {
1676
- // Arrange
1677
- const {renderer} = renderQuestion({
1678
- ...question2,
1679
- content:
1680
- "Input 1: [[☃ input-number 1]]\n\n" +
1681
- "Input 2: [[☃ input-number 2]]",
1682
- widgets: {
1683
- ...question2.widgets,
1684
- "input-number 2": {
1685
- ...question2.widgets["input-number 1"],
1686
- static: true,
1687
- },
1688
- },
1689
- });
1690
- const cb = jest.fn();
1691
-
1692
- // Act
1693
- renderer.setInputValue(["input-number 2"], "1000", cb);
1694
- jest.runOnlyPendingTimers();
1695
-
1696
- // Assert
1697
- expect(cb).toHaveBeenCalled();
1698
- });
1699
- });
1700
-
1701
- describe("getUserInputForWidgets", () => {
1702
- it("should return user input for all rendered widgets", () => {
1703
- // Arrange
1704
- const {renderer} = renderQuestion({
1705
- content:
1706
- "Input widget: [[\u2603 input-number 1]]\n\n" +
1707
- "Dropdown widget: [[\u2603 dropdown 1]]\n\n" +
1708
- "Image widget (won't have user input): [[\u2603 image 1]]\n\n" +
1709
- "Another input widget: [[\u2603 input-number 2]]",
1710
- widgets: {
1711
- "image 1": imageWidget,
1712
- "input-number 1": inputNumberWidget,
1713
- "input-number 2": inputNumberWidget,
1714
- "dropdown 1": dropdownWidget,
1715
- },
1716
- images: {},
1717
- });
1718
-
1719
- userEvent.type(screen.getAllByRole("textbox")[0], "100");
1720
- userEvent.type(screen.getAllByRole("textbox")[1], "200");
1721
-
1722
- // Open the dropdown and select the second (idx: 1) item
1723
- userEvent.click(screen.getByRole("button"));
1724
- jest.runOnlyPendingTimers();
1725
- userEvent.click(screen.getAllByRole("option")[1]);
1726
- jest.runOnlyPendingTimers();
1727
-
1728
- // Act
1729
- const userInput = renderer.getUserInputForWidgets();
1730
-
1731
- // Assert
1732
- expect(userInput).toMatchInlineSnapshot(`
1733
- {
1734
- "dropdown 1": {
1735
- "value": 1,
1736
- },
1737
- "image 1": null,
1738
- "input-number 1": {
1739
- "currentValue": "100",
1740
- },
1741
- "input-number 2": {
1742
- "currentValue": "200",
1743
- },
1744
- }
1745
- `);
1746
- });
1747
- });
1748
-
1749
- describe("examples", () => {
1750
- it("should return examples if all widgets return the same examples (or null)", () => {
1751
- // Arrange
1752
- const {renderer} = renderQuestion({
1753
- content:
1754
- "Input widget: [[\u2603 input-number 1]]\n\n" +
1755
- "Dropdown widget: [[\u2603 dropdown 1]]\n\n" +
1756
- "Image widget (won't have user input): [[\u2603 image 1]]\n\n" +
1757
- "Another input widget: [[\u2603 input-number 2]]",
1758
- widgets: {
1759
- "image 1": imageWidget,
1760
- "input-number 1": inputNumberWidget,
1761
- "input-number 2": inputNumberWidget,
1762
- "dropdown 1": dropdownWidget,
1763
- },
1764
- images: {},
1765
- });
1766
-
1767
- // Act
1768
- const examples = renderer.examples();
1769
-
1770
- // Assert
1771
- expect(examples).toMatchInlineSnapshot(`
1772
- [
1773
- "**Your answer should be** ",
1774
- "an integer, like $6$",
1775
- "a *proper* fraction, like $1/2$ or $6/10$",
1776
- "an *improper* fraction, like $10/7$ or $14/8$",
1777
- "a mixed number, like $1\\ 3/4$",
1778
- ]
1779
- `);
1780
- });
1781
-
1782
- it("should return nothing if widgets return the different examples", () => {
1783
- // NOTE(jeremy): I'm unsure why we don't return examples if the
1784
- // examples aren't the same, but this is current functionality so
1785
- // I'm adding this test to verify the current behaviour.
1786
-
1787
- // Arrange
1788
- const {renderer} = renderQuestion({
1789
- content:
1790
- "Input widget: [[\u2603 input-number 1]]\n\n" +
1791
- "Dropdown widget: [[\u2603 dropdown 1]]\n\n" +
1792
- "Image widget (won't have user input): [[\u2603 image 1]]\n\n" +
1793
- "Another input widget: [[\u2603 input-number 2]]",
1794
- widgets: {
1795
- "image 1": imageWidget,
1796
- "input-number 1": inputNumberWidget,
1797
- "input-number 2": {
1798
- ...inputNumberWidget,
1799
- options: {
1800
- ...inputNumberWidget.options,
1801
- answerType: "percent",
1802
- } as PerseusInputNumberWidgetOptions,
1803
- },
1804
- "dropdown 1": dropdownWidget,
1805
- },
1806
- images: {},
1807
- });
1808
-
1809
- // Act
1810
- const examples = renderer.examples();
1811
-
1812
- // Assert
1813
- expect(examples).toBeNull();
1814
- });
1815
- });
1816
- });