@khanacademy/perseus 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (503) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/dist/es/index.js +15 -1
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +11934 -0
  6. package/dist/index.js +15 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/strings.js +148 -1
  9. package/package.json +40 -4
  10. package/src/README.md +7 -0
  11. package/src/__stories__/item-renderer.stories.jsx +27 -0
  12. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  13. package/src/__testdata__/item-renderer_testdata.js +161 -0
  14. package/src/__testdata__/renderer_testdata.js +123 -0
  15. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
  16. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
  17. package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
  18. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
  19. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
  20. package/src/__tests__/a11y_test.jsx +304 -0
  21. package/src/__tests__/item-renderer_test.jsx +892 -0
  22. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  23. package/src/__tests__/mock-widget.jsx +35 -0
  24. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  25. package/src/__tests__/pure-markdown_test.js +552 -0
  26. package/src/__tests__/renderability_test.jsx +481 -0
  27. package/src/__tests__/renderer-api_test.jsx +333 -0
  28. package/src/__tests__/renderer_test.jsx +1792 -0
  29. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  30. package/src/__tests__/test-items/image-item.js +25 -0
  31. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  32. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  33. package/src/__tests__/test-items/table-item.js +30 -0
  34. package/src/__tests__/tex-wrangler_test.js +83 -0
  35. package/src/__tests__/util_test.js +41 -0
  36. package/src/a11y.js +46 -0
  37. package/src/all-widgets.js +9 -0
  38. package/src/article-renderer.jsx +215 -0
  39. package/src/asset-context.js +30 -0
  40. package/src/basic-widgets.js +13 -0
  41. package/src/components/__stories__/button-group.stories.jsx +61 -0
  42. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  43. package/src/components/__stories__/graph.stories.jsx +27 -0
  44. package/src/components/__stories__/graphie.stories.jsx +28 -0
  45. package/src/components/__stories__/hud.stories.jsx +36 -0
  46. package/src/components/__stories__/icon.stories.jsx +56 -0
  47. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  48. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  49. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  50. package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
  51. package/src/components/__stories__/lint.stories.jsx +40 -0
  52. package/src/components/__stories__/math-input.stories.jsx +29 -0
  53. package/src/components/__stories__/math-output.stories.jsx +24 -0
  54. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  55. package/src/components/__stories__/number-input.stories.jsx +42 -0
  56. package/src/components/__stories__/possible-answers.stories.jsx +22 -0
  57. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  58. package/src/components/__stories__/range-input.stories.jsx +28 -0
  59. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  60. package/src/components/__stories__/sortable.stories.jsx +73 -0
  61. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  62. package/src/components/__stories__/svg-image.stories.jsx +60 -0
  63. package/src/components/__stories__/tex.stories.jsx +18 -0
  64. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  65. package/src/components/__stories__/text-input.stories.jsx +34 -0
  66. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  67. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  68. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  69. package/src/components/__testdata__/sorter_testdata.js +20 -0
  70. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  71. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  72. package/src/components/__tests__/number-input_test.jsx +112 -0
  73. package/src/components/__tests__/sortable_test.jsx +234 -0
  74. package/src/components/__tests__/sorter_test.jsx +111 -0
  75. package/src/components/__tests__/zoomable_test.jsx +388 -0
  76. package/src/components/button-group.jsx +131 -0
  77. package/src/components/constants.js +15 -0
  78. package/src/components/fixed-to-responsive.jsx +163 -0
  79. package/src/components/graph.jsx +414 -0
  80. package/src/components/graphie-classes.jsx +148 -0
  81. package/src/components/graphie-movables.jsx +342 -0
  82. package/src/components/graphie.jsx +353 -0
  83. package/src/components/highlighting/highlightable-content.jsx +245 -0
  84. package/src/components/highlighting/highlights.js +208 -0
  85. package/src/components/highlighting/ranges.js +299 -0
  86. package/src/components/highlighting/types.js +73 -0
  87. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  88. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  89. package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
  90. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  91. package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
  92. package/src/components/highlighting/ui/types.js +21 -0
  93. package/src/components/highlighting/ui/util.js +166 -0
  94. package/src/components/highlighting/word-indexer.jsx +155 -0
  95. package/src/components/hud.jsx +179 -0
  96. package/src/components/icon.jsx +149 -0
  97. package/src/components/image-loader.jsx +175 -0
  98. package/src/components/info-tip/info-tip.jsx +149 -0
  99. package/src/components/info-tip.jsx +38 -0
  100. package/src/components/inline-icon.jsx +81 -0
  101. package/src/components/input-with-examples.jsx +231 -0
  102. package/src/components/lint.jsx +456 -0
  103. package/src/components/math-input.jsx +282 -0
  104. package/src/components/math-output.jsx +128 -0
  105. package/src/components/multi-button-group.jsx +137 -0
  106. package/src/components/new-tooltip/README.md +110 -0
  107. package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
  108. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
  109. package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
  110. package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
  111. package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
  112. package/src/components/new-tooltip/__tests__/util_test.js +47 -0
  113. package/src/components/new-tooltip/flexible-position.jsx +182 -0
  114. package/src/components/new-tooltip/new-tooltip.jsx +399 -0
  115. package/src/components/new-tooltip/style-constants.js +13 -0
  116. package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
  117. package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
  118. package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
  119. package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
  120. package/src/components/new-tooltip/types.js +183 -0
  121. package/src/components/new-tooltip/util.js +27 -0
  122. package/src/components/number-input.jsx +253 -0
  123. package/src/components/possible-answers.jsx +36 -0
  124. package/src/components/prop-check-box.jsx +78 -0
  125. package/src/components/range-input.jsx +61 -0
  126. package/src/components/simple-keypad-input.jsx +91 -0
  127. package/src/components/sortable.jsx +872 -0
  128. package/src/components/stub-tag-editor.jsx +53 -0
  129. package/src/components/svg-image.jsx +762 -0
  130. package/src/components/tex-buttons.jsx +322 -0
  131. package/src/components/tex.jsx +70 -0
  132. package/src/components/text-input.jsx +82 -0
  133. package/src/components/text-list-editor.jsx +157 -0
  134. package/src/components/tooltip.jsx +395 -0
  135. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  136. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  137. package/src/components/visibility-observer/visibility-observer.js +109 -0
  138. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  139. package/src/components/zoomable-tex.jsx +70 -0
  140. package/src/components/zoomable.jsx +304 -0
  141. package/src/components.js +17 -0
  142. package/src/dependencies.js +22 -0
  143. package/src/error-boundary.jsx +48 -0
  144. package/src/extra-widgets.js +75 -0
  145. package/src/gorgon/README +40 -0
  146. package/src/gorgon/__tests__/matcher_test.js +497 -0
  147. package/src/gorgon/__tests__/rule_test.js +101 -0
  148. package/src/gorgon/__tests__/rules_test.js +487 -0
  149. package/src/gorgon/__tests__/selector-parser_test.js +52 -0
  150. package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
  151. package/src/gorgon/gorgon.js +290 -0
  152. package/src/gorgon/proptypes.js +29 -0
  153. package/src/gorgon/rule.js +413 -0
  154. package/src/gorgon/rules/absolute-url.js +24 -0
  155. package/src/gorgon/rules/all-rules.js +72 -0
  156. package/src/gorgon/rules/blockquoted-math.js +10 -0
  157. package/src/gorgon/rules/blockquoted-widget.js +10 -0
  158. package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
  159. package/src/gorgon/rules/extra-content-spacing.js +12 -0
  160. package/src/gorgon/rules/heading-level-1.js +14 -0
  161. package/src/gorgon/rules/heading-level-skip.js +20 -0
  162. package/src/gorgon/rules/heading-sentence-case.js +11 -0
  163. package/src/gorgon/rules/heading-title-case.js +63 -0
  164. package/src/gorgon/rules/image-alt-text.js +21 -0
  165. package/src/gorgon/rules/image-in-table.js +10 -0
  166. package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
  167. package/src/gorgon/rules/image-widget.js +50 -0
  168. package/src/gorgon/rules/link-click-here.js +11 -0
  169. package/src/gorgon/rules/lint-utils.js +48 -0
  170. package/src/gorgon/rules/long-paragraph.js +14 -0
  171. package/src/gorgon/rules/math-adjacent.js +10 -0
  172. package/src/gorgon/rules/math-align-extra-break.js +11 -0
  173. package/src/gorgon/rules/math-align-linebreaks.js +43 -0
  174. package/src/gorgon/rules/math-empty.js +10 -0
  175. package/src/gorgon/rules/math-font-size.js +12 -0
  176. package/src/gorgon/rules/math-frac.js +10 -0
  177. package/src/gorgon/rules/math-nested.js +11 -0
  178. package/src/gorgon/rules/math-starts-with-space.js +12 -0
  179. package/src/gorgon/rules/math-text-empty.js +10 -0
  180. package/src/gorgon/rules/math-without-dollars.js +14 -0
  181. package/src/gorgon/rules/nested-lists.js +11 -0
  182. package/src/gorgon/rules/profanity.js +10 -0
  183. package/src/gorgon/rules/table-missing-cells.js +20 -0
  184. package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
  185. package/src/gorgon/rules/unescaped-dollar.js +10 -0
  186. package/src/gorgon/rules/widget-in-table.js +10 -0
  187. package/src/gorgon/selector.js +506 -0
  188. package/src/gorgon/tree-transformer.js +588 -0
  189. package/src/hint-renderer.jsx +159 -0
  190. package/src/hints-renderer.jsx +359 -0
  191. package/src/icon-paths.js +120 -0
  192. package/src/index.js +149 -2
  193. package/src/init.js +49 -0
  194. package/src/interaction-tracker.js +60 -0
  195. package/src/interactive2/__tests__/movable-mock.js +48 -0
  196. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  197. package/src/interactive2/interactive-util.js +152 -0
  198. package/src/interactive2/movable-helper-methods.js +160 -0
  199. package/src/interactive2/movable-line-options.js +392 -0
  200. package/src/interactive2/movable-line.js +322 -0
  201. package/src/interactive2/movable-point-options.js +190 -0
  202. package/src/interactive2/movable-point.jsx +604 -0
  203. package/src/interactive2/movable-polygon-options.js +351 -0
  204. package/src/interactive2/movable-polygon.js +367 -0
  205. package/src/interactive2/movable.js +273 -0
  206. package/src/interactive2/objective_.js +79 -0
  207. package/src/interactive2/types.js +3 -0
  208. package/src/interactive2/wrapped-defaults.js +86 -0
  209. package/src/interactive2/wrapped-ellipse.js +87 -0
  210. package/src/interactive2/wrapped-line.js +109 -0
  211. package/src/interactive2/wrapped-path.js +47 -0
  212. package/src/interactive2.js +91 -0
  213. package/src/item-renderer.jsx +518 -0
  214. package/src/item-version.js +12 -0
  215. package/src/jipt-paragraphs.js +44 -0
  216. package/src/jquery.mobile.vmouse.js +545 -0
  217. package/src/keypad-context.js +33 -0
  218. package/src/loading-context.js +20 -0
  219. package/src/logging/log.js +107 -0
  220. package/src/logging/log_test.js +41 -0
  221. package/src/mixins/changeable.jsx +115 -0
  222. package/src/mixins/editor-jsonify.jsx +13 -0
  223. package/src/mixins/provide-keypad.jsx +131 -0
  224. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  225. package/src/mixins/widget-prop-blacklist.js +36 -0
  226. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  227. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  228. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
  229. package/src/multi-items/__tests__/items_test.js +245 -0
  230. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  231. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  232. package/src/multi-items/__tests__/shapes_test.js +44 -0
  233. package/src/multi-items/__tests__/trees_test.js +316 -0
  234. package/src/multi-items/item-types.js +55 -0
  235. package/src/multi-items/items.js +176 -0
  236. package/src/multi-items/multi-renderer.jsx +542 -0
  237. package/src/multi-items/prop-type-builders.js +105 -0
  238. package/src/multi-items/shape-types.js +47 -0
  239. package/src/multi-items/shapes.js +53 -0
  240. package/src/multi-items/tree-types.js +42 -0
  241. package/src/multi-items/trees.js +345 -0
  242. package/src/multi-items.js +76 -0
  243. package/src/not-gorgon.js +129 -0
  244. package/src/perseus-api.jsx +270 -0
  245. package/src/perseus-env.js +26 -0
  246. package/src/perseus-error.js +19 -0
  247. package/src/perseus-markdown.jsx +434 -0
  248. package/src/perseus-types.js +1597 -0
  249. package/src/pure-markdown.js +346 -0
  250. package/src/question-paragraph.jsx +30 -0
  251. package/src/renderability.jsx +108 -0
  252. package/src/renderer.jsx +1931 -0
  253. package/src/server-item-renderer.jsx +467 -0
  254. package/src/sigfigs.jsx +492 -0
  255. package/src/styles/articles.less +748 -0
  256. package/src/styles/constants.js +119 -0
  257. package/src/styles/global-constants.js +350 -0
  258. package/src/styles/global-styles.js +307 -0
  259. package/src/styles/khan-exercise.css +1304 -0
  260. package/src/styles/media-queries.js +48 -0
  261. package/src/styles/perseus-renderer.less +1008 -0
  262. package/src/styles/shared.js +132 -0
  263. package/src/styles/util.less +47 -0
  264. package/src/styles/variables.boostrap.less +301 -0
  265. package/src/styles/variables.less +68 -0
  266. package/src/styles/variables.shared.less +356 -0
  267. package/src/styles/widgets/categorizer.less +39 -0
  268. package/src/styles/widgets/definition.less +86 -0
  269. package/src/styles/widgets/dropdown.less +3 -0
  270. package/src/styles/widgets/expression.less +147 -0
  271. package/src/styles/widgets/graded-group.less +36 -0
  272. package/src/styles/widgets/grapher.less +11 -0
  273. package/src/styles/widgets/group.less +19 -0
  274. package/src/styles/widgets/image.less +29 -0
  275. package/src/styles/widgets/interactive-graph.less +61 -0
  276. package/src/styles/widgets/label-image.less +41 -0
  277. package/src/styles/widgets/matcher.less +8 -0
  278. package/src/styles/widgets/matrix.less +120 -0
  279. package/src/styles/widgets/measurer.less +20 -0
  280. package/src/styles/widgets/orderer.less +97 -0
  281. package/src/styles/widgets/passage.less +151 -0
  282. package/src/styles/widgets/plotter.less +33 -0
  283. package/src/styles/widgets/radio.less +52 -0
  284. package/src/styles/widgets/reaction-diagram.less +35 -0
  285. package/src/styles/widgets/sequence.less +5 -0
  286. package/src/styles/widgets/sortable.less +127 -0
  287. package/src/styles/widgets/table.less +56 -0
  288. package/src/styles/widgets/transformer.less +28 -0
  289. package/src/styles/widgets/unit.less +14 -0
  290. package/src/tex-wrangler.js +176 -0
  291. package/src/traversal.jsx +141 -0
  292. package/src/types.js +538 -0
  293. package/src/util/a11y.js +24 -0
  294. package/src/util/answer-types.js +862 -0
  295. package/src/util/answer-types_test.js +115 -0
  296. package/src/util/colors.js +94 -0
  297. package/src/util/fix-passage-refs.jsx +122 -0
  298. package/src/util/graph-utils.js +16 -0
  299. package/src/util/graphie.js +1585 -0
  300. package/src/util/interactive.js +4071 -0
  301. package/src/util/katex-preprocess.js +16 -0
  302. package/src/util/math.js +296 -0
  303. package/src/util/mobile-native-utils.js +14 -0
  304. package/src/util/mobile-native-utils_test.js +24 -0
  305. package/src/util/register-all-widgets-for-testing.js +12 -0
  306. package/src/util/scroll-utils.js +162 -0
  307. package/src/util/sizing-utils.js +46 -0
  308. package/src/util/tex.js +203 -0
  309. package/src/util/url-utils.js +36 -0
  310. package/src/util.js +990 -0
  311. package/src/version.js +11 -0
  312. package/src/widget-container.jsx +174 -0
  313. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  314. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  315. package/src/widgets/__stories__/definition.stories.jsx +38 -0
  316. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  317. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  318. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  319. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  320. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  321. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  322. package/src/widgets/__stories__/group.stories.jsx +15 -0
  323. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  324. package/src/widgets/__stories__/image.stories.jsx +62 -0
  325. package/src/widgets/__stories__/input-number.stories.jsx +90 -0
  326. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  327. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  328. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  329. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  330. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  331. package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
  332. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  333. package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
  334. package/src/widgets/__stories__/passage.stories.jsx +19 -0
  335. package/src/widgets/__stories__/radio.stories.jsx +82 -0
  336. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  337. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  338. package/src/widgets/__stories__/video.stories.jsx +15 -0
  339. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  340. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  341. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  342. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  343. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  344. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  345. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  346. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  347. package/src/widgets/__testdata__/group_testdata.js +156 -0
  348. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  349. package/src/widgets/__testdata__/image_testdata.js +38 -0
  350. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  351. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  352. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  353. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  354. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  355. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  356. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  357. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  358. package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
  359. package/src/widgets/__testdata__/passage_testdata.js +71 -0
  360. package/src/widgets/__testdata__/radio_testdata.js +210 -0
  361. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  362. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  363. package/src/widgets/__testdata__/video_testdata.js +22 -0
  364. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
  365. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  366. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
  367. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
  368. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  369. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  370. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
  371. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
  372. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
  373. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
  374. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
  375. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  376. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
  377. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  378. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  379. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
  380. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  381. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
  382. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  383. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
  384. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  385. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  386. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  387. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
  388. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
  389. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  390. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  391. package/src/widgets/__tests__/definition_test.js +152 -0
  392. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  393. package/src/widgets/__tests__/explanation_test.js +199 -0
  394. package/src/widgets/__tests__/expression_test.jsx +439 -0
  395. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
  396. package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
  397. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  398. package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
  399. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  400. package/src/widgets/__tests__/group_test.jsx +512 -0
  401. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  402. package/src/widgets/__tests__/image_test.jsx +65 -0
  403. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  404. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  405. package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
  406. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  407. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  408. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  409. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  410. package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
  411. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  412. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  413. package/src/widgets/__tests__/passage_test.jsx +133 -0
  414. package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
  415. package/src/widgets/__tests__/radio_test.jsx +651 -0
  416. package/src/widgets/__tests__/renderQuestion.jsx +65 -0
  417. package/src/widgets/__tests__/unit_test.jsx +144 -0
  418. package/src/widgets/__tests__/video_test.jsx +55 -0
  419. package/src/widgets/categorizer.jsx +338 -0
  420. package/src/widgets/cs-program.jsx +228 -0
  421. package/src/widgets/definition.jsx +141 -0
  422. package/src/widgets/dropdown.jsx +143 -0
  423. package/src/widgets/explanation.jsx +315 -0
  424. package/src/widgets/expression.jsx +654 -0
  425. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  426. package/src/widgets/graded-group-set.jsx +285 -0
  427. package/src/widgets/graded-group.jsx +448 -0
  428. package/src/widgets/grapher/util.jsx +819 -0
  429. package/src/widgets/grapher.jsx +658 -0
  430. package/src/widgets/group.jsx +213 -0
  431. package/src/widgets/iframe.jsx +199 -0
  432. package/src/widgets/image.jsx +309 -0
  433. package/src/widgets/input-number.jsx +410 -0
  434. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  435. package/src/widgets/interaction/color-picker.jsx +69 -0
  436. package/src/widgets/interaction/dash-picker.jsx +46 -0
  437. package/src/widgets/interaction/element-container.jsx +97 -0
  438. package/src/widgets/interaction.jsx +742 -0
  439. package/src/widgets/interactive-graph.jsx +2821 -0
  440. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  441. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  442. package/src/widgets/label-image/answer-choices.jsx +311 -0
  443. package/src/widgets/label-image/marker.jsx +365 -0
  444. package/src/widgets/label-image/types.js +33 -0
  445. package/src/widgets/label-image.jsx +841 -0
  446. package/src/widgets/label-image_test.js +840 -0
  447. package/src/widgets/lights-puzzle.jsx +356 -0
  448. package/src/widgets/matcher.jsx +271 -0
  449. package/src/widgets/matrix.jsx +615 -0
  450. package/src/widgets/measurer.jsx +215 -0
  451. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  452. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  453. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  454. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  455. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  456. package/src/widgets/molecule.jsx +172 -0
  457. package/src/widgets/number-line.jsx +858 -0
  458. package/src/widgets/numeric-input.jsx +622 -0
  459. package/src/widgets/orderer.jsx +753 -0
  460. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
  461. package/src/widgets/passage/get-line-height-for-node.js +10 -0
  462. package/src/widgets/passage/passage-markdown.jsx +416 -0
  463. package/src/widgets/passage-ref-target.jsx +70 -0
  464. package/src/widgets/passage-ref.jsx +198 -0
  465. package/src/widgets/passage.jsx +601 -0
  466. package/src/widgets/plotter.jsx +1198 -0
  467. package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
  468. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
  469. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
  470. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
  471. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
  472. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
  473. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
  474. package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
  475. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
  476. package/src/widgets/radio/base-radio.jsx +642 -0
  477. package/src/widgets/radio/choice-icon.jsx +468 -0
  478. package/src/widgets/radio/choice-none-above.jsx +54 -0
  479. package/src/widgets/radio/choice.jsx +692 -0
  480. package/src/widgets/radio/choice.jsx.fixture.js +64 -0
  481. package/src/widgets/radio/cross-out-button.jsx +136 -0
  482. package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
  483. package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
  484. package/src/widgets/radio/focus-ring.jsx +51 -0
  485. package/src/widgets/radio/option-status.jsx +89 -0
  486. package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
  487. package/src/widgets/radio/util.js +32 -0
  488. package/src/widgets/radio/util_test.js +19 -0
  489. package/src/widgets/radio/widget.jsx +470 -0
  490. package/src/widgets/radio.jsx +156 -0
  491. package/src/widgets/reaction-diagram.jsx +136 -0
  492. package/src/widgets/sequence.jsx +140 -0
  493. package/src/widgets/simulator.jsx +808 -0
  494. package/src/widgets/sorter.jsx +146 -0
  495. package/src/widgets/table.jsx +395 -0
  496. package/src/widgets/transformer.jsx +2870 -0
  497. package/src/widgets/unit.jsx +371 -0
  498. package/src/widgets/video-transcript-link.jsx +84 -0
  499. package/src/widgets/video-transcript-link_test.js +211 -0
  500. package/src/widgets/video.jsx +131 -0
  501. package/src/widgets.js +431 -0
  502. package/src/zoom.js +539 -0
  503. package/src/__tests__/index.test.js +0 -14
@@ -0,0 +1,651 @@
1
+ // @flow
2
+
3
+ import {screen, fireEvent} from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import _ from "underscore";
6
+ import "@testing-library/jest-dom"; // Imports custom mathers
7
+
8
+ import {getMockUniqueId} from "../../../../../testing/mock-unique-id.js";
9
+ import {clone} from "../../../../../testing/object-utils.js";
10
+ import {testDependencies} from "../../../../../testing/test-dependencies.js";
11
+ import * as Dependencies from "../../dependencies.js";
12
+ import {
13
+ passageWidget,
14
+ questionAndAnswer,
15
+ multiChoiceQuestionAndAnswer,
16
+ } from "../__testdata__/radio_testdata.js";
17
+
18
+ import {renderQuestion} from "./renderQuestion.jsx";
19
+
20
+ import type {
21
+ PerseusRenderer,
22
+ RadioWidget,
23
+ PerseusRadioWidgetOptions,
24
+ } from "../../perseus-types.js";
25
+ import type {APIOptions} from "../../types.js";
26
+
27
+ const waitForTooltip = () => {
28
+ // We need to wait !!three!! render passes for the NewTooltip to be
29
+ // fully rendered.
30
+ jest.runOnlyPendingTimers();
31
+ jest.runOnlyPendingTimers();
32
+ jest.runOnlyPendingTimers();
33
+ };
34
+
35
+ const answer = (index: number) => {
36
+ const radios = screen.getAllByRole("radio");
37
+ // userEvent.click() returns a very hard-to-understand error if passed an
38
+ // element that is null/undefined (ie. if the index is invalid) so we
39
+ // manually check and throw here to protect future me, and others :)
40
+ if (index > radios.length) {
41
+ throw new Error("Invalid array index for radio");
42
+ }
43
+ userEvent.click(radios[index]);
44
+ };
45
+
46
+ describe("single-choice question", () => {
47
+ beforeEach(() => {
48
+ jest.spyOn(_, "uniqueId").mockImplementation(getMockUniqueId());
49
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
50
+ testDependencies,
51
+ );
52
+ });
53
+
54
+ const [question, correct, incorrect] = questionAndAnswer;
55
+
56
+ describe.each([[true], [false]])("satStyling: %s", (satStyling) => {
57
+ const apiOptions: APIOptions = {
58
+ satStyling,
59
+ };
60
+
61
+ describe.each([[true], [false]])(
62
+ "reviewMode: %s",
63
+ (reviewMode: boolean) => {
64
+ it("should snapshot the same", () => {
65
+ // Arrange & Act
66
+ const {container} = renderQuestion(question, apiOptions, {
67
+ reviewMode,
68
+ });
69
+
70
+ // Assert
71
+ expect(container).toMatchSnapshot("first render");
72
+ });
73
+
74
+ it("should snapshot the same with correct answer", () => {
75
+ // Arrange
76
+ const {container} = renderQuestion(question, apiOptions);
77
+
78
+ // Act
79
+ answer(correct);
80
+
81
+ // Assert
82
+ expect(container).toMatchSnapshot("correct answer");
83
+ });
84
+
85
+ it("should snapshot the same with incorrect answer", () => {
86
+ // Arrange
87
+ const {container} = renderQuestion(question, apiOptions);
88
+
89
+ // Act
90
+ answer(incorrect[0]);
91
+
92
+ // Assert
93
+ expect(container).toMatchSnapshot("incorrect answer");
94
+ });
95
+
96
+ it("should accept the right answer (mouse)", () => {
97
+ // Arrange
98
+ const {renderer} = renderQuestion(question, apiOptions, {
99
+ reviewMode,
100
+ });
101
+
102
+ // Act
103
+ answer(correct);
104
+
105
+ // Assert
106
+ expect(renderer).toHaveBeenAnsweredCorrectly();
107
+ });
108
+
109
+ it("should accept the right answer (touch)", () => {
110
+ // Arrange
111
+ const {renderer} = renderQuestion(question, apiOptions);
112
+ const correctRadio = screen.getAllByRole("radio")[correct];
113
+
114
+ // Act
115
+ fireEvent.touchStart(correctRadio);
116
+ fireEvent.touchEnd(correctRadio);
117
+ // We're using fireEvent.click() because mobile browsers synthesize
118
+ // click events from matching touchStart/touchEnd events. user-event
119
+ // does not support touch events so we have to do this ourselves.
120
+ // eslint-disable-next-line testing-library/prefer-user-event
121
+ fireEvent.click(correctRadio);
122
+
123
+ // Assert
124
+ expect(renderer).toHaveBeenAnsweredCorrectly();
125
+ });
126
+
127
+ it.each(incorrect)(
128
+ "should reject incorrect answer - choice %d",
129
+ (incorrect: number) => {
130
+ // Arrange
131
+ const {renderer} = renderQuestion(question, apiOptions);
132
+
133
+ // Act
134
+ answer(incorrect);
135
+
136
+ // Assert
137
+ expect(renderer).toHaveBeenAnsweredIncorrectly();
138
+ },
139
+ );
140
+
141
+ it("calling .focus() on the renderer should succeed", () => {
142
+ // Arrange
143
+ const {renderer} = renderQuestion(question, apiOptions, {
144
+ reviewMode,
145
+ });
146
+
147
+ // Act
148
+ const gotFocus = renderer.focus();
149
+
150
+ // Assert
151
+ expect(gotFocus).toBeTrue();
152
+ });
153
+
154
+ it("should deselect incorrect selected choices", () => {
155
+ // Arrange
156
+ const {renderer} = renderQuestion(question, apiOptions, {
157
+ reviewMode,
158
+ });
159
+
160
+ // Act
161
+ // Since this is a single-select setup, just select the first
162
+ // incorrect choice.
163
+ answer(incorrect[0]);
164
+ renderer.deselectIncorrectSelectedChoices();
165
+
166
+ // Assert
167
+ screen.getAllByRole("radio").forEach((r) => {
168
+ expect(r).not.toBeChecked();
169
+ });
170
+ });
171
+
172
+ it("should disable all radio inputs when static is true", () => {
173
+ // Arrange
174
+ const staticQuestion = {
175
+ ...question,
176
+ widgets: {
177
+ ...question.widgets,
178
+ "radio 1": {
179
+ ...question.widgets["radio 1"],
180
+ static: true,
181
+ },
182
+ },
183
+ };
184
+ renderQuestion(staticQuestion, apiOptions, {reviewMode});
185
+
186
+ // Act
187
+ answer(correct);
188
+
189
+ // Assert
190
+ // Everything's read-only so no selections made
191
+ screen.getAllByRole("radio").forEach((r) => {
192
+ expect(r).toBeDisabled();
193
+ });
194
+ });
195
+ },
196
+ );
197
+
198
+ it("should be able to navigate down by keyboard", () => {
199
+ // Arrange
200
+ renderQuestion(question, apiOptions);
201
+
202
+ // Act
203
+ userEvent.type(screen.getAllByRole("radio")[0], "{arrowdown}");
204
+
205
+ // Assert
206
+ expect(screen.getAllByRole("radio")[1]).toHaveFocus();
207
+ });
208
+
209
+ it("should be able to navigate up by keyboard", () => {
210
+ // Arrange
211
+ renderQuestion(question, apiOptions);
212
+
213
+ // Act
214
+ userEvent.type(screen.getAllByRole("radio")[0], "{arrowup}");
215
+
216
+ // Assert
217
+ expect(screen.getAllByRole("radio")[3]).toHaveFocus();
218
+ });
219
+
220
+ it("should be able to navigate through 'None of the above' choice by keyboard", () => {
221
+ // Arrange
222
+ const q = clone(question);
223
+ ((q.widgets["radio 1"]
224
+ .options: any): PerseusRadioWidgetOptions).choices[3].isNoneOfTheAbove = true;
225
+ renderQuestion(q, apiOptions);
226
+
227
+ // Act
228
+ userEvent.type(screen.getAllByRole("radio")[0], "{arrowup}");
229
+ userEvent.type(screen.getAllByRole("radio")[3], "{arrowup}");
230
+
231
+ // Assert
232
+ expect(screen.getAllByRole("radio")[2]).toHaveFocus();
233
+ });
234
+
235
+ it.each([
236
+ ["No", "Yes"],
237
+ ["False", "True"],
238
+ ])("should enforce ordering for common answers: %j", (...answers) => {
239
+ // Arrange
240
+ const q = clone(question);
241
+ ((q.widgets["radio 1"]
242
+ .options: any): PerseusRadioWidgetOptions).choices = answers.map(
243
+ (answer, idx) => ({
244
+ content: answer,
245
+ correct: idx === 1, // Correct answer is the "truthy" item
246
+ }),
247
+ );
248
+
249
+ // Act
250
+ const {renderer} = renderQuestion(q, apiOptions);
251
+ // We click on the first item, which was the second (index == 1)
252
+ // item in the original choices. But because of enforced ordering,
253
+ // it is now at the top of the list (and thus our correct answer).
254
+ userEvent.click(screen.getAllByRole("radio")[0]);
255
+
256
+ // Assert
257
+ const items = screen.getAllByRole("listitem");
258
+ expect(items[0]).toHaveTextContent(answers[1]);
259
+ expect(items[1]).toHaveTextContent(answers[0]);
260
+ expect(renderer).toHaveBeenAnsweredCorrectly();
261
+ });
262
+
263
+ it("should not change ordering of non-common answers", () => {
264
+ // Arrange
265
+ const answers = ["Last", "First"];
266
+ const q = clone(question);
267
+ ((q.widgets["radio 1"]
268
+ .options: any): PerseusRadioWidgetOptions).choices = answers.map(
269
+ (answer, idx) => ({
270
+ content: answer,
271
+ correct: idx === 1,
272
+ }),
273
+ );
274
+
275
+ // Act
276
+ renderQuestion(q, apiOptions);
277
+
278
+ // Assert
279
+ const items = screen.getAllByRole("listitem");
280
+ expect(items[0]).toHaveTextContent(answers[0]);
281
+ expect(items[1]).toHaveTextContent(answers[1]);
282
+ });
283
+
284
+ it("should transform inline passage-refs to references to passage widgets", () => {
285
+ // Arrange
286
+
287
+ // Add a passage widget to the question content and then reference it
288
+ // in an answer.
289
+ const questionWithPassage = clone(question);
290
+ questionWithPassage.content = `[[\u2603 passage 1]]\n\nNow what's the answer?\n\n[[\u2603 radio 1]]`;
291
+ questionWithPassage.widgets["passage 1"] = passageWidget;
292
+ const radioOptions = questionWithPassage.widgets["radio 1"];
293
+ // HACK(jeremy): Flow doesn't know what type these options are because
294
+ // we've extracted them out of a generic `PerseusJson` blob and all the
295
+ // `widget` items are just type `Widget`.
296
+ (radioOptions.options: any).choices[0].content =
297
+ '{{passage-ref 1 1 "Reference 1 here"}}';
298
+
299
+ // Act
300
+ renderQuestion(questionWithPassage, apiOptions);
301
+ // Passage refs use `_.defer()` to update their reference ranges.
302
+ jest.runOnlyPendingTimers();
303
+
304
+ // Assert
305
+ // By using a `passage-ref` in a choice, we should now have the
306
+ // reference on the radio we used it on.
307
+ // NOTE: We get by listitem role here because the 'radio' role
308
+ // element does not contain the HTML that provides the label
309
+ // for it.
310
+ const passageRefRadio = screen.getAllByRole("listitem")[0];
311
+ expect(passageRefRadio).toHaveTextContent("lines NaN–NaN");
312
+ });
313
+
314
+ it("Should render option statuses (rationales) for selected choices", () => {
315
+ // Arrange
316
+ const {renderer} = renderQuestion(question, apiOptions);
317
+
318
+ // Act
319
+ answer(incorrect[0]);
320
+ renderer.showRationalesForCurrentlySelectedChoices();
321
+
322
+ // Assert
323
+ expect(
324
+ screen.queryAllByTestId(
325
+ `perseus-radio-rationale-content-${incorrect[0]}`,
326
+ ),
327
+ ).toHaveLength(1);
328
+ });
329
+
330
+ describe("cross-out is enabled", () => {
331
+ const crossOutApiOptions = {
332
+ ...apiOptions,
333
+ crossOutEnabled: true,
334
+ };
335
+
336
+ it("should render cross-out menu button", () => {
337
+ // Arrange
338
+
339
+ // Act
340
+ renderQuestion(question, crossOutApiOptions);
341
+
342
+ // Assert
343
+ expect(
344
+ screen.getByRole("button", {
345
+ name: /Open menu for Choice B/,
346
+ }),
347
+ ).toBeVisible();
348
+ });
349
+
350
+ it("should open the cross-out menu when button clicked", () => {
351
+ // Arrange
352
+ renderQuestion(question, crossOutApiOptions);
353
+
354
+ // Act
355
+ userEvent.click(
356
+ screen.getByRole("button", {
357
+ name: /Open menu for Choice B/,
358
+ }),
359
+ );
360
+ waitForTooltip();
361
+
362
+ // Assert
363
+ expect(
364
+ screen.getByRole("button", {
365
+ name: /Cross out Choice B/,
366
+ }),
367
+ ).toBeVisible();
368
+ });
369
+
370
+ it("should open the cross-out menu when focused and spacebar pressed", () => {
371
+ // Arrange
372
+ renderQuestion(question, crossOutApiOptions);
373
+ userEvent.tab(); // Choice icon
374
+ userEvent.tab(); // Cross-out menu ellipsis
375
+
376
+ // Act
377
+ userEvent.keyboard("{space}");
378
+ waitForTooltip();
379
+
380
+ // Assert
381
+ expect(
382
+ screen.getByRole("button", {
383
+ name: /Cross out Choice A/,
384
+ }),
385
+ ).toHaveFocus();
386
+ });
387
+
388
+ it("should cross-out selection and dismiss button when clicked", () => {
389
+ // Arrange
390
+ renderQuestion(question, crossOutApiOptions);
391
+ userEvent.click(
392
+ screen.getByRole("button", {
393
+ name: /Open menu for Choice B/,
394
+ }),
395
+ );
396
+ waitForTooltip();
397
+
398
+ // Act
399
+ userEvent.click(
400
+ screen.getByRole("button", {
401
+ name: /Cross out Choice B/,
402
+ }),
403
+ );
404
+ jest.runOnlyPendingTimers();
405
+
406
+ // Assert
407
+ expect(
408
+ screen.queryAllByRole("button", {
409
+ name: /Cross out Choice B/,
410
+ }),
411
+ ).toHaveLength(0);
412
+
413
+ // The choice widget maintains a "preview" of what the opposite
414
+ // state is for use in the "Cross-out button", but it's lazy in
415
+ // updating the preview (only changing it when the button is
416
+ // revealed). So once we cross-out a choice, we have two elements
417
+ // in the DOM with the `Cross out` text. :shrug:
418
+ expect(
419
+ screen.queryAllByText("(Choice B, Crossed out)"),
420
+ ).toHaveLength(2);
421
+ });
422
+
423
+ it("should dismiss cross-out button with {tab} key", () => {
424
+ // Arrange
425
+ renderQuestion(question, crossOutApiOptions);
426
+ userEvent.tab(); // Choice icon
427
+ userEvent.tab(); // Cross-out menu ellipsis
428
+
429
+ // Open
430
+ userEvent.keyboard("{space}");
431
+ waitForTooltip();
432
+
433
+ expect(
434
+ screen.queryAllByRole("button", {
435
+ name: /Cross out Choice A/,
436
+ }),
437
+ ).toHaveLength(1);
438
+
439
+ // Act
440
+ userEvent.tab();
441
+
442
+ // Assert
443
+ expect(
444
+ screen.queryAllByRole("button", {
445
+ name: /Cross out Choice B/,
446
+ }),
447
+ ).toHaveLength(0);
448
+ });
449
+
450
+ it("should dismiss cross-out button with {Escape} key", () => {
451
+ // Arrange
452
+ renderQuestion(question, crossOutApiOptions);
453
+
454
+ userEvent.tab(); // Choice icon
455
+ userEvent.tab(); // Cross-out menu ellipsis
456
+ userEvent.keyboard("{space}"); // Open tooltip
457
+
458
+ // Global escape handler only set up after a clock tick
459
+ jest.runOnlyPendingTimers();
460
+
461
+ // Act
462
+ userEvent.keyboard("{esc}");
463
+
464
+ // Assert
465
+ expect(
466
+ screen.queryAllByRole("button", {
467
+ name: /Cross out Choice B/,
468
+ }),
469
+ ).toHaveLength(0);
470
+ });
471
+
472
+ it("should dismiss cross-out button with click outside tooltip", () => {
473
+ // Arrange
474
+ renderQuestion(question, crossOutApiOptions);
475
+
476
+ userEvent.tab(); // Choice icon
477
+ userEvent.tab(); // Cross-out menu ellipsis
478
+ userEvent.keyboard("{space}"); // Open tooltip
479
+
480
+ // Global escape handler only set up after a clock tick
481
+ jest.runOnlyPendingTimers();
482
+
483
+ // Act
484
+ userEvent.click((document.body: any));
485
+
486
+ // Assert
487
+ expect(
488
+ screen.queryAllByRole("button", {
489
+ name: /Cross out Choice B/,
490
+ }),
491
+ ).toHaveLength(0);
492
+ });
493
+ });
494
+ });
495
+
496
+ it("should be invalid when first rendered", () => {
497
+ // Arrange
498
+ const apiOptions: APIOptions = {
499
+ crossOutEnabled: false,
500
+ };
501
+
502
+ // Act
503
+ const {renderer} = renderQuestion(question, apiOptions);
504
+
505
+ // Assert
506
+ expect(renderer).toHaveInvalidInput();
507
+ });
508
+
509
+ it("Should render correct option select statuses (rationales) when review mode enabled", () => {
510
+ // Arrange
511
+ const apiOptions: APIOptions = {
512
+ crossOutEnabled: false,
513
+ };
514
+ renderQuestion(question, apiOptions, {reviewMode: true});
515
+
516
+ // Act
517
+ answer(correct);
518
+
519
+ // Assert
520
+ expect(screen.getAllByText("Correct (selected)")).toHaveLength(1);
521
+ });
522
+
523
+ it("Should render incorrect option select statuses (rationales) when review mode enabled", () => {
524
+ // Arrange
525
+ const apiOptions: APIOptions = {
526
+ crossOutEnabled: false,
527
+ };
528
+ renderQuestion(question, apiOptions, {reviewMode: true});
529
+
530
+ // Act
531
+ answer(incorrect[0]);
532
+
533
+ // Assert
534
+ expect(screen.getAllByText("Incorrect (selected)")).toHaveLength(1);
535
+ });
536
+ });
537
+
538
+ describe("multi-choice question", () => {
539
+ const [question, correct, incorrect, invalid] =
540
+ multiChoiceQuestionAndAnswer;
541
+
542
+ const apiOptions: APIOptions = {
543
+ satStyling: false,
544
+ };
545
+
546
+ beforeEach(() => {
547
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
548
+ testDependencies,
549
+ );
550
+ });
551
+
552
+ it("should accept the right answer", () => {
553
+ // Arrange
554
+ const {renderer} = renderQuestion(question, apiOptions);
555
+
556
+ // Act
557
+ const boxes = screen.getAllByRole("checkbox");
558
+ correct.forEach((i) => userEvent.click(boxes[i]));
559
+
560
+ // Assert
561
+ expect(renderer).toHaveBeenAnsweredCorrectly();
562
+ });
563
+
564
+ it("should snapshot the same when invalid", () => {
565
+ // Arrange
566
+ const {container} = renderQuestion(question, apiOptions);
567
+
568
+ // Act
569
+ const boxes = screen.getAllByRole("checkbox");
570
+ incorrect[0].forEach((i) => userEvent.click(boxes[i]));
571
+
572
+ // Assert
573
+ expect(container).toMatchSnapshot("invalid state");
574
+ });
575
+
576
+ it("should be invalid when incorrect number of choices selected", () => {
577
+ // Arrange
578
+ const apiOptions: APIOptions = {
579
+ crossOutEnabled: false,
580
+ };
581
+ const radio1Widget = ((question.widgets["radio 1"]: any): RadioWidget);
582
+ const radioOptions = radio1Widget.options;
583
+
584
+ const multipleCorrectChoicesQuestion: PerseusRenderer = {
585
+ ...question,
586
+ widgets: {
587
+ ...question.widgets,
588
+ "radio 1": ({
589
+ ...radio1Widget,
590
+ options: {
591
+ ...radioOptions,
592
+ choices: [
593
+ {content: "$x=-6$", correct: true},
594
+ {content: "$x=4$", correct: true},
595
+ {content: "$x=7$", correct: false},
596
+ {
597
+ content: "There is no such input value.",
598
+ isNoneOfTheAbove: true,
599
+ correct: false,
600
+ },
601
+ ],
602
+ },
603
+ }: RadioWidget),
604
+ },
605
+ };
606
+
607
+ const {renderer} = renderQuestion(
608
+ multipleCorrectChoicesQuestion,
609
+ apiOptions,
610
+ );
611
+
612
+ // Act
613
+ const boxes = screen.getAllByRole("checkbox");
614
+ userEvent.click(boxes[3]); // incorrect
615
+
616
+ // Assert
617
+ expect(renderer).toHaveInvalidInput(
618
+ "Please choose the correct number of answers",
619
+ );
620
+ });
621
+
622
+ it.each(incorrect)(
623
+ "should reject an incorrect answer - test #%#",
624
+ (...choices) => {
625
+ // Arrange
626
+ const {renderer} = renderQuestion(question, apiOptions);
627
+
628
+ // Act
629
+ const boxes = screen.getAllByRole("checkbox");
630
+ choices.forEach((i) => userEvent.click(boxes[i]));
631
+
632
+ // Assert
633
+ expect(renderer).toHaveBeenAnsweredIncorrectly();
634
+ },
635
+ );
636
+
637
+ it.each(invalid)(
638
+ "should reject an invalid answer - test #%#",
639
+ (...choices) => {
640
+ // Arrange
641
+ const {renderer} = renderQuestion(question, apiOptions);
642
+
643
+ // Act
644
+ const boxes = screen.getAllByRole("checkbox");
645
+ choices.forEach((i) => userEvent.click(boxes[i]));
646
+
647
+ // Assert
648
+ expect(renderer).toHaveInvalidInput();
649
+ },
650
+ );
651
+ });