@khanacademy/perseus 19.1.5 → 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 (478) hide show
  1. package/dist/es/index.js +1 -1
  2. package/dist/index.js +1 -1
  3. package/package.json +11 -8
  4. package/.babelrc.js +0 -8
  5. package/.eslintrc.js +0 -12
  6. package/CHANGELOG.md +0 -1432
  7. package/src/__stories__/article-renderer.stories.tsx +0 -100
  8. package/src/__stories__/hints-renderer.stories.tsx +0 -55
  9. package/src/__stories__/server-item-renderer.stories.tsx +0 -44
  10. package/src/__testdata__/article-renderer.testdata.ts +0 -212
  11. package/src/__testdata__/graphie.testdata.ts +0 -51
  12. package/src/__testdata__/renderer.testdata.ts +0 -274
  13. package/src/__testdata__/server-item-renderer.testdata.ts +0 -171
  14. package/src/__tests__/__snapshots__/perseus-markdown.test.ts.snap +0 -507
  15. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +0 -525
  16. package/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap +0 -285
  17. package/src/__tests__/a11y.test.ts +0 -303
  18. package/src/__tests__/article-renderer.test.tsx +0 -130
  19. package/src/__tests__/error-boundary.test.tsx +0 -76
  20. package/src/__tests__/extract-perseus-data.test.ts +0 -1027
  21. package/src/__tests__/init.test.ts +0 -10
  22. package/src/__tests__/jipt-paragraphs.test.ts +0 -32
  23. package/src/__tests__/mock-asset-loading-widget.tsx +0 -53
  24. package/src/__tests__/mock-widget.tsx +0 -33
  25. package/src/__tests__/perseus-markdown.test.ts +0 -764
  26. package/src/__tests__/renderability.test.ts +0 -480
  27. package/src/__tests__/renderer-api.test.tsx +0 -302
  28. package/src/__tests__/renderer.test.tsx +0 -1816
  29. package/src/__tests__/server-item-renderer.test.tsx +0 -582
  30. package/src/__tests__/test-items/image-item.ts +0 -24
  31. package/src/__tests__/test-items/input-number-1-item.ts +0 -26
  32. package/src/__tests__/test-items/input-number-2-item.ts +0 -38
  33. package/src/__tests__/test-items/table-item.ts +0 -29
  34. package/src/__tests__/tex-wrangler.test.ts +0 -82
  35. package/src/__tests__/util.test.ts +0 -40
  36. package/src/__tests__/widget-container.test.tsx +0 -126
  37. package/src/__tests__/widgets.test.ts +0 -91
  38. package/src/a11y.ts +0 -45
  39. package/src/all-widgets.ts +0 -9
  40. package/src/article-renderer.tsx +0 -237
  41. package/src/asset-context.ts +0 -30
  42. package/src/basic-widgets.ts +0 -16
  43. package/src/components/__stories__/button-group.stories.tsx +0 -55
  44. package/src/components/__stories__/fixed-to-responsive.stories.tsx +0 -141
  45. package/src/components/__stories__/graph.stories.tsx +0 -28
  46. package/src/components/__stories__/graphie.stories.tsx +0 -33
  47. package/src/components/__stories__/hud.stories.tsx +0 -35
  48. package/src/components/__stories__/icon.stories.tsx +0 -63
  49. package/src/components/__stories__/image-loader.stories.tsx +0 -63
  50. package/src/components/__stories__/info-tip.stories.tsx +0 -31
  51. package/src/components/__stories__/inline-icon.stories.tsx +0 -40
  52. package/src/components/__stories__/input-with-examples.stories.tsx +0 -53
  53. package/src/components/__stories__/lint.stories.tsx +0 -86
  54. package/src/components/__stories__/math-input.stories.tsx +0 -43
  55. package/src/components/__stories__/math-output.stories.tsx +0 -23
  56. package/src/components/__stories__/multi-button-group.stories.tsx +0 -66
  57. package/src/components/__stories__/number-input.stories.tsx +0 -41
  58. package/src/components/__stories__/prop-check-box.stories.tsx +0 -65
  59. package/src/components/__stories__/range-input.stories.tsx +0 -29
  60. package/src/components/__stories__/simple-keypad-input.stories.tsx +0 -27
  61. package/src/components/__stories__/sortable.stories.tsx +0 -77
  62. package/src/components/__stories__/stub-tag-editor.stories.tsx +0 -45
  63. package/src/components/__stories__/svg-image.stories.tsx +0 -74
  64. package/src/components/__stories__/tex.stories.tsx +0 -23
  65. package/src/components/__stories__/text-input.stories.tsx +0 -33
  66. package/src/components/__stories__/text-list-editor.stories.tsx +0 -32
  67. package/src/components/__stories__/tooltip.stories.tsx +0 -52
  68. package/src/components/__stories__/zoomable-tex.stories.tsx +0 -41
  69. package/src/components/__stories__/zoomable.stories.tsx +0 -49
  70. package/src/components/__testdata__/sorter.testdata.ts +0 -19
  71. package/src/components/__tests__/__snapshots__/sortable.test.tsx.snap +0 -163
  72. package/src/components/__tests__/__snapshots__/sorter.test.tsx.snap +0 -265
  73. package/src/components/__tests__/math-input.test.tsx +0 -147
  74. package/src/components/__tests__/number-input.test.tsx +0 -128
  75. package/src/components/__tests__/sortable.test.tsx +0 -269
  76. package/src/components/__tests__/sorter.test.tsx +0 -114
  77. package/src/components/__tests__/zoomable.test.tsx +0 -399
  78. package/src/components/button-group.tsx +0 -130
  79. package/src/components/constants.ts +0 -14
  80. package/src/components/fixed-to-responsive.tsx +0 -169
  81. package/src/components/graph.tsx +0 -444
  82. package/src/components/graphie-classes.ts +0 -153
  83. package/src/components/graphie-movables.ts +0 -338
  84. package/src/components/graphie.tsx +0 -400
  85. package/src/components/highlighting/highlightable-content.tsx +0 -240
  86. package/src/components/highlighting/highlights.ts +0 -207
  87. package/src/components/highlighting/ranges.ts +0 -304
  88. package/src/components/highlighting/types.ts +0 -73
  89. package/src/components/highlighting/ui/highlight-renderer.tsx +0 -172
  90. package/src/components/highlighting/ui/highlight-set-renderer.tsx +0 -185
  91. package/src/components/highlighting/ui/highlight-tooltip.tsx +0 -141
  92. package/src/components/highlighting/ui/highlighting-ui.tsx +0 -143
  93. package/src/components/highlighting/ui/selection-tracker.tsx +0 -186
  94. package/src/components/highlighting/ui/types.ts +0 -21
  95. package/src/components/highlighting/ui/util.ts +0 -165
  96. package/src/components/highlighting/word-indexer.tsx +0 -152
  97. package/src/components/hud.tsx +0 -167
  98. package/src/components/icon.tsx +0 -141
  99. package/src/components/image-loader.tsx +0 -177
  100. package/src/components/info-tip/info-tip.tsx +0 -144
  101. package/src/components/info-tip.tsx +0 -39
  102. package/src/components/inline-icon.tsx +0 -60
  103. package/src/components/input-with-examples.tsx +0 -249
  104. package/src/components/lint.tsx +0 -459
  105. package/src/components/math-input.tsx +0 -474
  106. package/src/components/math-output.tsx +0 -134
  107. package/src/components/multi-button-group.tsx +0 -135
  108. package/src/components/number-input.tsx +0 -249
  109. package/src/components/prop-check-box.tsx +0 -79
  110. package/src/components/range-input.tsx +0 -60
  111. package/src/components/simple-keypad-input.tsx +0 -88
  112. package/src/components/sortable.tsx +0 -958
  113. package/src/components/stub-tag-editor.tsx +0 -53
  114. package/src/components/svg-image.tsx +0 -779
  115. package/src/components/tex.tsx +0 -67
  116. package/src/components/text-input.tsx +0 -129
  117. package/src/components/text-list-editor.tsx +0 -166
  118. package/src/components/tooltip.tsx +0 -410
  119. package/src/components/visibility-observer/__stories__/visibility-observer.stories.tsx +0 -141
  120. package/src/components/visibility-observer/create-visibility-observer.ts +0 -108
  121. package/src/components/visibility-observer/get-scroll-parent.ts +0 -46
  122. package/src/components/visibility-observer/visibility-observer.tsx +0 -124
  123. package/src/components/zoomable-tex.tsx +0 -78
  124. package/src/components/zoomable.tsx +0 -304
  125. package/src/components.ts +0 -15
  126. package/src/definition-context.tsx +0 -59
  127. package/src/dependencies.ts +0 -40
  128. package/src/error-boundary.tsx +0 -70
  129. package/src/extra-widgets.ts +0 -75
  130. package/src/hint-renderer.tsx +0 -151
  131. package/src/hints-renderer.tsx +0 -372
  132. package/src/icon-paths.ts +0 -119
  133. package/src/index.ts +0 -171
  134. package/src/init.ts +0 -56
  135. package/src/interaction-tracker.ts +0 -62
  136. package/src/interactive2/__tests__/movable-mock.ts +0 -55
  137. package/src/interactive2/__tests__/movable-point.test.ts +0 -144
  138. package/src/interactive2/interactive-util.test.ts +0 -93
  139. package/src/interactive2/interactive-util.ts +0 -159
  140. package/src/interactive2/movable-helper-methods.ts +0 -166
  141. package/src/interactive2/movable-line-options.ts +0 -419
  142. package/src/interactive2/movable-line.ts +0 -307
  143. package/src/interactive2/movable-point-options.ts +0 -224
  144. package/src/interactive2/movable-point.tsx +0 -585
  145. package/src/interactive2/movable-polygon-options.ts +0 -389
  146. package/src/interactive2/movable-polygon.ts +0 -360
  147. package/src/interactive2/movable.test.ts +0 -249
  148. package/src/interactive2/movable.ts +0 -437
  149. package/src/interactive2/objective_.ts +0 -79
  150. package/src/interactive2/types.ts +0 -17
  151. package/src/interactive2/wrapped-defaults.ts +0 -87
  152. package/src/interactive2/wrapped-ellipse.ts +0 -94
  153. package/src/interactive2/wrapped-line.ts +0 -116
  154. package/src/interactive2/wrapped-path.ts +0 -51
  155. package/src/interactive2.ts +0 -70
  156. package/src/item-version.ts +0 -10
  157. package/src/jipt-paragraphs.ts +0 -49
  158. package/src/jquery.mobile.vmouse.js +0 -545
  159. package/src/loading-context.ts +0 -21
  160. package/src/logging/log.test.ts +0 -39
  161. package/src/logging/log.ts +0 -97
  162. package/src/mixins/__tests__/widget-prop-denylist.test.ts +0 -21
  163. package/src/mixins/changeable.ts +0 -133
  164. package/src/mixins/editor-jsonify.ts +0 -13
  165. package/src/mixins/widget-jsonify-deprecated.ts +0 -23
  166. package/src/mixins/widget-prop-denylist.ts +0 -48
  167. package/src/multi-items/__stories__/multi-renderer.stories.tsx +0 -83
  168. package/src/multi-items/__testdata__/multi-renderer.testdata.ts +0 -172
  169. package/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +0 -1164
  170. package/src/multi-items/__tests__/items.test.ts +0 -240
  171. package/src/multi-items/__tests__/multi-renderer.test.tsx +0 -914
  172. package/src/multi-items/__tests__/prop-type-builders.test.ts +0 -133
  173. package/src/multi-items/__tests__/shapes.test.ts +0 -43
  174. package/src/multi-items/__tests__/trees.test.ts +0 -317
  175. package/src/multi-items/item-types.ts +0 -56
  176. package/src/multi-items/items.ts +0 -176
  177. package/src/multi-items/multi-renderer.tsx +0 -569
  178. package/src/multi-items/prop-type-builders.ts +0 -106
  179. package/src/multi-items/shape-types.ts +0 -52
  180. package/src/multi-items/shapes.ts +0 -52
  181. package/src/multi-items/tree-types.ts +0 -47
  182. package/src/multi-items/trees.ts +0 -360
  183. package/src/multi-items.ts +0 -75
  184. package/src/perseus-api.tsx +0 -188
  185. package/src/perseus-env.ts +0 -27
  186. package/src/perseus-error.ts +0 -17
  187. package/src/perseus-markdown.tsx +0 -405
  188. package/src/perseus-types.ts +0 -1366
  189. package/src/perseus-version.ts +0 -9
  190. package/src/question-paragraph.tsx +0 -34
  191. package/src/renderability.ts +0 -109
  192. package/src/renderer.tsx +0 -2000
  193. package/src/server-item-renderer.tsx +0 -495
  194. package/src/sigfigs.ts +0 -490
  195. package/src/styles/articles.less +0 -749
  196. package/src/styles/constants.ts +0 -94
  197. package/src/styles/global-constants.ts +0 -343
  198. package/src/styles/global-styles.ts +0 -306
  199. package/src/styles/khan-exercise.css +0 -1468
  200. package/src/styles/media-queries.ts +0 -47
  201. package/src/styles/perseus-renderer.less +0 -732
  202. package/src/styles/shared.ts +0 -130
  203. package/src/styles/util.less +0 -47
  204. package/src/styles/variables.boostrap.less +0 -277
  205. package/src/styles/variables.less +0 -56
  206. package/src/styles/variables.shared.less +0 -340
  207. package/src/styles/widgets/categorizer.less +0 -38
  208. package/src/styles/widgets/dropdown.less +0 -3
  209. package/src/styles/widgets/expression.less +0 -65
  210. package/src/styles/widgets/graded-group.less +0 -36
  211. package/src/styles/widgets/grapher.less +0 -12
  212. package/src/styles/widgets/group.less +0 -19
  213. package/src/styles/widgets/image.less +0 -54
  214. package/src/styles/widgets/interactive-graph.less +0 -64
  215. package/src/styles/widgets/label-image.less +0 -57
  216. package/src/styles/widgets/matcher.less +0 -8
  217. package/src/styles/widgets/matrix.less +0 -126
  218. package/src/styles/widgets/measurer.less +0 -20
  219. package/src/styles/widgets/orderer.less +0 -98
  220. package/src/styles/widgets/passage.less +0 -154
  221. package/src/styles/widgets/plotter.less +0 -31
  222. package/src/styles/widgets/radio.less +0 -58
  223. package/src/styles/widgets/reaction-diagram.less +0 -35
  224. package/src/styles/widgets/sequence.less +0 -5
  225. package/src/styles/widgets/sortable.less +0 -113
  226. package/src/styles/widgets/table.less +0 -57
  227. package/src/styles/widgets/unit.less +0 -15
  228. package/src/tex-wrangler.ts +0 -182
  229. package/src/translation-linter.ts +0 -126
  230. package/src/traversal.ts +0 -138
  231. package/src/types.ts +0 -513
  232. package/src/util/a11y.ts +0 -23
  233. package/src/util/answer-types.test.ts +0 -114
  234. package/src/util/answer-types.ts +0 -876
  235. package/src/util/colors.ts +0 -93
  236. package/src/util/drawing-transform.test.ts +0 -67
  237. package/src/util/drawing-transform.ts +0 -102
  238. package/src/util/extract-perseus-data.ts +0 -539
  239. package/src/util/fix-passage-refs.ts +0 -121
  240. package/src/util/geometry.test.ts +0 -75
  241. package/src/util/geometry.ts +0 -312
  242. package/src/util/graph-bounds.ts +0 -23
  243. package/src/util/graph-utils.ts +0 -15
  244. package/src/util/graphie.test.ts +0 -1277
  245. package/src/util/graphie.ts +0 -1576
  246. package/src/util/interactive.ts +0 -4174
  247. package/src/util/interval.ts +0 -4
  248. package/src/util/katex-preprocess.ts +0 -14
  249. package/src/util/math.test.ts +0 -11
  250. package/src/util/math.ts +0 -294
  251. package/src/util/mobile-native-utils.test.ts +0 -22
  252. package/src/util/mobile-native-utils.ts +0 -12
  253. package/src/util/parse-perseus-json.test.ts +0 -10
  254. package/src/util/parse-perseus-json.ts +0 -13
  255. package/src/util/react-render.tsx +0 -27
  256. package/src/util/register-all-widgets-for-testing.ts +0 -13
  257. package/src/util/scroll-utils.ts +0 -155
  258. package/src/util/sizing-utils.ts +0 -46
  259. package/src/util/tex.ts +0 -73
  260. package/src/util/url-utils.ts +0 -34
  261. package/src/util.graphie.test.ts +0 -45
  262. package/src/util.graphie.ts +0 -59
  263. package/src/util.ts +0 -966
  264. package/src/version.ts +0 -10
  265. package/src/widget-container.tsx +0 -199
  266. package/src/widgets/__stories__/categorizer.stories.tsx +0 -14
  267. package/src/widgets/__stories__/cs-program.stories.tsx +0 -14
  268. package/src/widgets/__stories__/definition.stories.tsx +0 -114
  269. package/src/widgets/__stories__/dropdown.stories.tsx +0 -14
  270. package/src/widgets/__stories__/explanation.stories.tsx +0 -18
  271. package/src/widgets/__stories__/expression.stories.tsx +0 -145
  272. package/src/widgets/__stories__/graded-group-set.stories.tsx +0 -31
  273. package/src/widgets/__stories__/graded-group.stories.tsx +0 -31
  274. package/src/widgets/__stories__/grapher.stories.tsx +0 -46
  275. package/src/widgets/__stories__/group.stories.tsx +0 -14
  276. package/src/widgets/__stories__/iframe.stories.tsx +0 -14
  277. package/src/widgets/__stories__/image.stories.tsx +0 -85
  278. package/src/widgets/__stories__/input-number.stories.tsx +0 -101
  279. package/src/widgets/__stories__/interaction.stories.tsx +0 -14
  280. package/src/widgets/__stories__/interactive-graph.stories.tsx +0 -60
  281. package/src/widgets/__stories__/label-image.stories.tsx +0 -130
  282. package/src/widgets/__stories__/matcher.stories.tsx +0 -14
  283. package/src/widgets/__stories__/matrix.stories.tsx +0 -14
  284. package/src/widgets/__stories__/number-line.stories.tsx +0 -14
  285. package/src/widgets/__stories__/numeric-input.stories.tsx +0 -105
  286. package/src/widgets/__stories__/orderer.stories.tsx +0 -18
  287. package/src/widgets/__stories__/passage-ref.stories.tsx +0 -33
  288. package/src/widgets/__stories__/passage.stories.tsx +0 -39
  289. package/src/widgets/__stories__/plotter.stories.tsx +0 -27
  290. package/src/widgets/__stories__/radio.stories.tsx +0 -86
  291. package/src/widgets/__stories__/random-widgets.stories.tsx +0 -59
  292. package/src/widgets/__stories__/sorter.stories.tsx +0 -14
  293. package/src/widgets/__stories__/test-keypad-context-wrapper.tsx +0 -66
  294. package/src/widgets/__stories__/unit.stories.tsx +0 -28
  295. package/src/widgets/__stories__/video-transcript-link.stories.tsx +0 -23
  296. package/src/widgets/__stories__/video.stories.tsx +0 -22
  297. package/src/widgets/__testdata__/base-radio.testdata.ts +0 -21
  298. package/src/widgets/__testdata__/categorizer.testdata.ts +0 -89
  299. package/src/widgets/__testdata__/cs-program.testdata.ts +0 -27
  300. package/src/widgets/__testdata__/dropdown.testdata.ts +0 -71
  301. package/src/widgets/__testdata__/explanation.testdata.ts +0 -85
  302. package/src/widgets/__testdata__/expression.testdata.ts +0 -198
  303. package/src/widgets/__testdata__/graded-group-set.testdata.ts +0 -356
  304. package/src/widgets/__testdata__/graded-group.testdata.ts +0 -77
  305. package/src/widgets/__testdata__/grapher.testdata.ts +0 -374
  306. package/src/widgets/__testdata__/group.testdata.ts +0 -153
  307. package/src/widgets/__testdata__/iframe.testdata.ts +0 -35
  308. package/src/widgets/__testdata__/image.testdata.ts +0 -36
  309. package/src/widgets/__testdata__/input-number.testdata.ts +0 -72
  310. package/src/widgets/__testdata__/interaction.testdata.ts +0 -262
  311. package/src/widgets/__testdata__/interactive-graph-random.testdata.ts +0 -269
  312. package/src/widgets/__testdata__/interactive-graph.testdata.ts +0 -644
  313. package/src/widgets/__testdata__/label-image.testdata.ts +0 -308
  314. package/src/widgets/__testdata__/matcher.testdata.ts +0 -33
  315. package/src/widgets/__testdata__/matrix.testdata.ts +0 -28
  316. package/src/widgets/__testdata__/number-line.testdata.ts +0 -30
  317. package/src/widgets/__testdata__/numeric-input.testdata.ts +0 -265
  318. package/src/widgets/__testdata__/orderer.testdata.ts +0 -123
  319. package/src/widgets/__testdata__/passage-ref.testdata.ts +0 -101
  320. package/src/widgets/__testdata__/passage.testdata.ts +0 -95
  321. package/src/widgets/__testdata__/plotter.testdata.ts +0 -36
  322. package/src/widgets/__testdata__/radio.testdata.ts +0 -324
  323. package/src/widgets/__testdata__/randomizers.ts +0 -51
  324. package/src/widgets/__testdata__/sorter.testdata.ts +0 -19
  325. package/src/widgets/__testdata__/video.testdata.ts +0 -61
  326. package/src/widgets/__tests__/__snapshots__/categorizer.test.ts.snap +0 -940
  327. package/src/widgets/__tests__/__snapshots__/cs-program.test.ts.snap +0 -67
  328. package/src/widgets/__tests__/__snapshots__/definition.test.ts.snap +0 -97
  329. package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +0 -133
  330. package/src/widgets/__tests__/__snapshots__/explanation.test.ts.snap +0 -425
  331. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt.test.ts.snap +0 -729
  332. package/src/widgets/__tests__/__snapshots__/graded-group-set.test.ts.snap +0 -339
  333. package/src/widgets/__tests__/__snapshots__/graded-group.test.ts.snap +0 -801
  334. package/src/widgets/__tests__/__snapshots__/grapher.test.ts.snap +0 -2673
  335. package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +0 -1211
  336. package/src/widgets/__tests__/__snapshots__/iframe.test.ts.snap +0 -61
  337. package/src/widgets/__tests__/__snapshots__/image.test.ts.snap +0 -378
  338. package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +0 -495
  339. package/src/widgets/__tests__/__snapshots__/interaction.test.ts.snap +0 -292
  340. package/src/widgets/__tests__/__snapshots__/interactive-graph.test.ts.snap +0 -2889
  341. package/src/widgets/__tests__/__snapshots__/matcher.test.tsx.snap +0 -883
  342. package/src/widgets/__tests__/__snapshots__/matrix.test.ts.snap +0 -514
  343. package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +0 -814
  344. package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +0 -470
  345. package/src/widgets/__tests__/__snapshots__/orderer.test.ts.snap +0 -243
  346. package/src/widgets/__tests__/__snapshots__/passage-ref.test.ts.snap +0 -434
  347. package/src/widgets/__tests__/__snapshots__/passage.test.tsx.snap +0 -1956
  348. package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +0 -827
  349. package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +0 -4237
  350. package/src/widgets/__tests__/__snapshots__/unit.test.tsx.snap +0 -20
  351. package/src/widgets/__tests__/__snapshots__/video.test.ts.snap +0 -153
  352. package/src/widgets/__tests__/categorizer.test.ts +0 -203
  353. package/src/widgets/__tests__/cs-program.test.ts +0 -43
  354. package/src/widgets/__tests__/definition.test.ts +0 -136
  355. package/src/widgets/__tests__/dropdown.test.ts +0 -105
  356. package/src/widgets/__tests__/explanation.test.ts +0 -198
  357. package/src/widgets/__tests__/expression-mobile.test.tsx +0 -363
  358. package/src/widgets/__tests__/expression.test.tsx +0 -484
  359. package/src/widgets/__tests__/graded-group-set-jipt.test.ts +0 -30
  360. package/src/widgets/__tests__/graded-group-set.test.ts +0 -288
  361. package/src/widgets/__tests__/graded-group.test.ts +0 -232
  362. package/src/widgets/__tests__/grapher.cypress.ts +0 -576
  363. package/src/widgets/__tests__/grapher.test.ts +0 -33
  364. package/src/widgets/__tests__/group.test.tsx +0 -510
  365. package/src/widgets/__tests__/iframe.test.ts +0 -45
  366. package/src/widgets/__tests__/image.test.ts +0 -62
  367. package/src/widgets/__tests__/input-number.test.ts +0 -354
  368. package/src/widgets/__tests__/interaction.test.ts +0 -31
  369. package/src/widgets/__tests__/interactive-graph.cypress.ts +0 -815
  370. package/src/widgets/__tests__/interactive-graph.test.ts +0 -106
  371. package/src/widgets/__tests__/matcher.test.tsx +0 -176
  372. package/src/widgets/__tests__/matrix.test.ts +0 -82
  373. package/src/widgets/__tests__/number-line.test.ts +0 -79
  374. package/src/widgets/__tests__/numeric-input.test.ts +0 -575
  375. package/src/widgets/__tests__/orderer.test.ts +0 -73
  376. package/src/widgets/__tests__/passage-ref.test.ts +0 -168
  377. package/src/widgets/__tests__/passage.test.tsx +0 -224
  378. package/src/widgets/__tests__/plotter.test.tsx +0 -69
  379. package/src/widgets/__tests__/radio/base-radio.test.tsx +0 -470
  380. package/src/widgets/__tests__/radio/choice-icon.test.tsx +0 -90
  381. package/src/widgets/__tests__/radio/choice.test.tsx +0 -310
  382. package/src/widgets/__tests__/radio/focus-ring.test.tsx +0 -22
  383. package/src/widgets/__tests__/radio/option-status.test.tsx +0 -77
  384. package/src/widgets/__tests__/radio.test.ts +0 -807
  385. package/src/widgets/__tests__/randomizers.test.ts +0 -53
  386. package/src/widgets/__tests__/renderQuestion.tsx +0 -101
  387. package/src/widgets/__tests__/unit.test.tsx +0 -168
  388. package/src/widgets/__tests__/video.test.ts +0 -69
  389. package/src/widgets/categorizer.tsx +0 -344
  390. package/src/widgets/cs-program.tsx +0 -227
  391. package/src/widgets/definition.tsx +0 -121
  392. package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +0 -30
  393. package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +0 -70
  394. package/src/widgets/deprecated-standin/__tests__/deprecated-standin.test.ts +0 -93
  395. package/src/widgets/deprecated-standin/index.tsx +0 -58
  396. package/src/widgets/dropdown.tsx +0 -142
  397. package/src/widgets/explanation.tsx +0 -317
  398. package/src/widgets/expression.tsx +0 -707
  399. package/src/widgets/graded-group-answer-bar.tsx +0 -142
  400. package/src/widgets/graded-group-set.tsx +0 -338
  401. package/src/widgets/graded-group.tsx +0 -471
  402. package/src/widgets/grapher/util.tsx +0 -799
  403. package/src/widgets/grapher.tsx +0 -660
  404. package/src/widgets/group.tsx +0 -195
  405. package/src/widgets/iframe.tsx +0 -195
  406. package/src/widgets/image.tsx +0 -312
  407. package/src/widgets/input-number.tsx +0 -412
  408. package/src/widgets/interaction/arrow-picker.tsx +0 -38
  409. package/src/widgets/interaction/color-picker.tsx +0 -69
  410. package/src/widgets/interaction/dash-picker.tsx +0 -45
  411. package/src/widgets/interaction/element-container.tsx +0 -100
  412. package/src/widgets/interaction.tsx +0 -826
  413. package/src/widgets/interactive-graph.tsx +0 -2597
  414. package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +0 -91
  415. package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +0 -62
  416. package/src/widgets/label-image/__stories__/marker.stories.tsx +0 -171
  417. package/src/widgets/label-image/__tests__/answer-pill.test.tsx +0 -75
  418. package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +0 -39
  419. package/src/widgets/label-image/answer-choices.tsx +0 -88
  420. package/src/widgets/label-image/answer-pill.tsx +0 -103
  421. package/src/widgets/label-image/hide-answers-toggle.tsx +0 -40
  422. package/src/widgets/label-image/marker.tsx +0 -268
  423. package/src/widgets/label-image/strings.ts +0 -5
  424. package/src/widgets/label-image/types.ts +0 -27
  425. package/src/widgets/label-image.test.ts +0 -928
  426. package/src/widgets/label-image.tsx +0 -819
  427. package/src/widgets/lights-puzzle.tsx +0 -359
  428. package/src/widgets/matcher.tsx +0 -298
  429. package/src/widgets/matrix.tsx +0 -634
  430. package/src/widgets/measurer.tsx +0 -222
  431. package/src/widgets/molecule/__tests__/layout.test.ts +0 -162
  432. package/src/widgets/molecule/__tests__/parser.test.ts +0 -155
  433. package/src/widgets/molecule/molecule-drawing.ts +0 -173
  434. package/src/widgets/molecule/molecule-layout.ts +0 -362
  435. package/src/widgets/molecule/smiles-parser.ts +0 -296
  436. package/src/widgets/molecule.tsx +0 -172
  437. package/src/widgets/number-line.tsx +0 -862
  438. package/src/widgets/numeric-input.tsx +0 -576
  439. package/src/widgets/orderer.tsx +0 -786
  440. package/src/widgets/passage/__tests__/passage-markdown.test.ts +0 -376
  441. package/src/widgets/passage/passage-markdown.tsx +0 -475
  442. package/src/widgets/passage-ref-target.tsx +0 -76
  443. package/src/widgets/passage-ref.tsx +0 -213
  444. package/src/widgets/passage.tsx +0 -582
  445. package/src/widgets/plotter.tsx +0 -1194
  446. package/src/widgets/radio/__stories__/base-radio.stories.tsx +0 -196
  447. package/src/widgets/radio/__stories__/choice-icon.stories.tsx +0 -166
  448. package/src/widgets/radio/__stories__/choice-none-above.stories.tsx +0 -40
  449. package/src/widgets/radio/__stories__/choice.stories.tsx +0 -137
  450. package/src/widgets/radio/__stories__/focus-ring.stories.tsx +0 -43
  451. package/src/widgets/radio/__stories__/option-status.stories.tsx +0 -103
  452. package/src/widgets/radio/base-radio.tsx +0 -511
  453. package/src/widgets/radio/choice-a11y-text.ts +0 -61
  454. package/src/widgets/radio/choice-icon/choice-icon.tsx +0 -218
  455. package/src/widgets/radio/choice-icon/cross-out-line.tsx +0 -51
  456. package/src/widgets/radio/choice-icon/shared-styles.ts +0 -15
  457. package/src/widgets/radio/choice-none-above.tsx +0 -56
  458. package/src/widgets/radio/choice.jsx.fixture.ts +0 -45
  459. package/src/widgets/radio/choice.tsx +0 -359
  460. package/src/widgets/radio/focus-ring.tsx +0 -57
  461. package/src/widgets/radio/option-status.tsx +0 -98
  462. package/src/widgets/radio/util.test.ts +0 -18
  463. package/src/widgets/radio/util.ts +0 -31
  464. package/src/widgets/radio/widget.tsx +0 -520
  465. package/src/widgets/radio.ts +0 -157
  466. package/src/widgets/reaction-diagram.tsx +0 -144
  467. package/src/widgets/sequence.tsx +0 -142
  468. package/src/widgets/simulator.tsx +0 -829
  469. package/src/widgets/sorter.tsx +0 -155
  470. package/src/widgets/table.tsx +0 -393
  471. package/src/widgets/unit.tsx +0 -374
  472. package/src/widgets/video-transcript-link.test.tsx +0 -213
  473. package/src/widgets/video-transcript-link.tsx +0 -84
  474. package/src/widgets/video.tsx +0 -135
  475. package/src/widgets.ts +0 -489
  476. package/src/zoom.ts +0 -555
  477. package/tsconfig-build.json +0 -26
  478. 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
- });