@khanacademy/perseus 0.0.1 → 0.2.1

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 (507) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/dist/es/index.css +4361 -0
  4. package/dist/es/index.js +7 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +4361 -0
  7. package/dist/index.js +7 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +148 -46
  10. package/package.json +50 -4
  11. package/src/README.md +35 -0
  12. package/src/__stories__/item-renderer.stories.jsx +27 -0
  13. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  14. package/src/__testdata__/item-renderer_testdata.js +161 -0
  15. package/src/__testdata__/renderer_testdata.js +123 -0
  16. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
  17. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
  18. package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
  19. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
  20. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
  21. package/src/__tests__/a11y_test.jsx +304 -0
  22. package/src/__tests__/item-renderer_test.jsx +892 -0
  23. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  24. package/src/__tests__/mock-widget.jsx +35 -0
  25. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  26. package/src/__tests__/pure-markdown_test.js +552 -0
  27. package/src/__tests__/renderability_test.jsx +481 -0
  28. package/src/__tests__/renderer-api_test.jsx +333 -0
  29. package/src/__tests__/renderer_test.jsx +1792 -0
  30. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  31. package/src/__tests__/test-items/image-item.js +25 -0
  32. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  33. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  34. package/src/__tests__/test-items/table-item.js +30 -0
  35. package/src/__tests__/tex-wrangler_test.js +83 -0
  36. package/src/__tests__/util_test.js +41 -0
  37. package/src/a11y.js +46 -0
  38. package/src/all-widgets.js +9 -0
  39. package/src/article-renderer.jsx +215 -0
  40. package/src/asset-context.js +30 -0
  41. package/src/basic-widgets.js +13 -0
  42. package/src/components/__stories__/button-group.stories.jsx +61 -0
  43. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  44. package/src/components/__stories__/graph.stories.jsx +27 -0
  45. package/src/components/__stories__/graphie.stories.jsx +28 -0
  46. package/src/components/__stories__/hud.stories.jsx +36 -0
  47. package/src/components/__stories__/icon.stories.jsx +56 -0
  48. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  49. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  50. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  51. package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
  52. package/src/components/__stories__/lint.stories.jsx +40 -0
  53. package/src/components/__stories__/math-input.stories.jsx +29 -0
  54. package/src/components/__stories__/math-output.stories.jsx +24 -0
  55. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  56. package/src/components/__stories__/number-input.stories.jsx +42 -0
  57. package/src/components/__stories__/possible-answers.stories.jsx +22 -0
  58. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  59. package/src/components/__stories__/range-input.stories.jsx +28 -0
  60. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  61. package/src/components/__stories__/sortable.stories.jsx +73 -0
  62. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  63. package/src/components/__stories__/svg-image.stories.jsx +60 -0
  64. package/src/components/__stories__/tex.stories.jsx +18 -0
  65. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  66. package/src/components/__stories__/text-input.stories.jsx +34 -0
  67. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  68. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  69. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  70. package/src/components/__testdata__/sorter_testdata.js +20 -0
  71. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  72. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  73. package/src/components/__tests__/number-input_test.jsx +112 -0
  74. package/src/components/__tests__/sortable_test.jsx +234 -0
  75. package/src/components/__tests__/sorter_test.jsx +111 -0
  76. package/src/components/__tests__/zoomable_test.jsx +388 -0
  77. package/src/components/button-group.jsx +131 -0
  78. package/src/components/constants.js +15 -0
  79. package/src/components/fixed-to-responsive.jsx +164 -0
  80. package/src/components/graph.jsx +415 -0
  81. package/src/components/graphie-classes.jsx +148 -0
  82. package/src/components/graphie-movables.jsx +342 -0
  83. package/src/components/graphie.jsx +353 -0
  84. package/src/components/highlighting/highlightable-content.jsx +245 -0
  85. package/src/components/highlighting/highlights.js +208 -0
  86. package/src/components/highlighting/ranges.js +299 -0
  87. package/src/components/highlighting/types.js +73 -0
  88. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  89. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  90. package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
  91. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  92. package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
  93. package/src/components/highlighting/ui/types.js +21 -0
  94. package/src/components/highlighting/ui/util.js +166 -0
  95. package/src/components/highlighting/word-indexer.jsx +155 -0
  96. package/src/components/hud.jsx +179 -0
  97. package/src/components/icon.jsx +149 -0
  98. package/src/components/image-loader.jsx +175 -0
  99. package/src/components/info-tip/info-tip.jsx +146 -0
  100. package/src/components/info-tip.jsx +38 -0
  101. package/src/components/inline-icon.jsx +81 -0
  102. package/src/components/input-with-examples.jsx +231 -0
  103. package/src/components/lint.jsx +454 -0
  104. package/src/components/math-input.jsx +282 -0
  105. package/src/components/math-output.jsx +128 -0
  106. package/src/components/multi-button-group.jsx +137 -0
  107. package/src/components/new-tooltip/README.md +110 -0
  108. package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
  109. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
  110. package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
  111. package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
  112. package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
  113. package/src/components/new-tooltip/__tests__/util_test.js +47 -0
  114. package/src/components/new-tooltip/flexible-position.jsx +182 -0
  115. package/src/components/new-tooltip/new-tooltip.jsx +399 -0
  116. package/src/components/new-tooltip/style-constants.js +13 -0
  117. package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
  118. package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
  119. package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
  120. package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
  121. package/src/components/new-tooltip/types.js +183 -0
  122. package/src/components/new-tooltip/util.js +27 -0
  123. package/src/components/number-input.jsx +253 -0
  124. package/src/components/possible-answers.jsx +36 -0
  125. package/src/components/prop-check-box.jsx +80 -0
  126. package/src/components/range-input.jsx +61 -0
  127. package/src/components/simple-keypad-input.jsx +91 -0
  128. package/src/components/sortable.jsx +873 -0
  129. package/src/components/stub-tag-editor.jsx +54 -0
  130. package/src/components/svg-image.jsx +762 -0
  131. package/src/components/tex-buttons.jsx +322 -0
  132. package/src/components/tex.jsx +70 -0
  133. package/src/components/text-input.jsx +82 -0
  134. package/src/components/text-list-editor.jsx +157 -0
  135. package/src/components/tooltip.jsx +395 -0
  136. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  137. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  138. package/src/components/visibility-observer/visibility-observer.js +109 -0
  139. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  140. package/src/components/zoomable-tex.jsx +70 -0
  141. package/src/components/zoomable.jsx +304 -0
  142. package/src/components.js +17 -0
  143. package/src/dependencies.js +22 -0
  144. package/src/error-boundary.jsx +48 -0
  145. package/src/extra-widgets.js +75 -0
  146. package/src/gorgon/README +40 -0
  147. package/src/gorgon/__tests__/matcher_test.js +497 -0
  148. package/src/gorgon/__tests__/rule_test.js +101 -0
  149. package/src/gorgon/__tests__/rules_test.js +487 -0
  150. package/src/gorgon/__tests__/selector-parser_test.js +52 -0
  151. package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
  152. package/src/gorgon/gorgon.js +290 -0
  153. package/src/gorgon/proptypes.js +29 -0
  154. package/src/gorgon/rule.js +413 -0
  155. package/src/gorgon/rules/absolute-url.js +24 -0
  156. package/src/gorgon/rules/all-rules.js +72 -0
  157. package/src/gorgon/rules/blockquoted-math.js +10 -0
  158. package/src/gorgon/rules/blockquoted-widget.js +10 -0
  159. package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
  160. package/src/gorgon/rules/extra-content-spacing.js +12 -0
  161. package/src/gorgon/rules/heading-level-1.js +14 -0
  162. package/src/gorgon/rules/heading-level-skip.js +20 -0
  163. package/src/gorgon/rules/heading-sentence-case.js +11 -0
  164. package/src/gorgon/rules/heading-title-case.js +63 -0
  165. package/src/gorgon/rules/image-alt-text.js +21 -0
  166. package/src/gorgon/rules/image-in-table.js +10 -0
  167. package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
  168. package/src/gorgon/rules/image-widget.js +50 -0
  169. package/src/gorgon/rules/link-click-here.js +11 -0
  170. package/src/gorgon/rules/lint-utils.js +48 -0
  171. package/src/gorgon/rules/long-paragraph.js +14 -0
  172. package/src/gorgon/rules/math-adjacent.js +10 -0
  173. package/src/gorgon/rules/math-align-extra-break.js +11 -0
  174. package/src/gorgon/rules/math-align-linebreaks.js +43 -0
  175. package/src/gorgon/rules/math-empty.js +10 -0
  176. package/src/gorgon/rules/math-font-size.js +12 -0
  177. package/src/gorgon/rules/math-frac.js +10 -0
  178. package/src/gorgon/rules/math-nested.js +11 -0
  179. package/src/gorgon/rules/math-starts-with-space.js +12 -0
  180. package/src/gorgon/rules/math-text-empty.js +10 -0
  181. package/src/gorgon/rules/math-without-dollars.js +14 -0
  182. package/src/gorgon/rules/nested-lists.js +11 -0
  183. package/src/gorgon/rules/profanity.js +10 -0
  184. package/src/gorgon/rules/table-missing-cells.js +20 -0
  185. package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
  186. package/src/gorgon/rules/unescaped-dollar.js +10 -0
  187. package/src/gorgon/rules/widget-in-table.js +10 -0
  188. package/src/gorgon/selector.js +506 -0
  189. package/src/gorgon/tree-transformer.js +588 -0
  190. package/src/hint-renderer.jsx +159 -0
  191. package/src/hints-renderer.jsx +359 -0
  192. package/src/icon-paths.js +120 -0
  193. package/src/index.js +149 -2
  194. package/src/init.js +49 -0
  195. package/src/interaction-tracker.js +60 -0
  196. package/src/interactive2/__tests__/movable-mock.js +48 -0
  197. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  198. package/src/interactive2/interactive-util.js +152 -0
  199. package/src/interactive2/movable-helper-methods.js +160 -0
  200. package/src/interactive2/movable-line-options.js +392 -0
  201. package/src/interactive2/movable-line.js +322 -0
  202. package/src/interactive2/movable-point-options.js +190 -0
  203. package/src/interactive2/movable-point.jsx +603 -0
  204. package/src/interactive2/movable-polygon-options.js +351 -0
  205. package/src/interactive2/movable-polygon.js +368 -0
  206. package/src/interactive2/movable.js +273 -0
  207. package/src/interactive2/objective_.js +79 -0
  208. package/src/interactive2/types.js +3 -0
  209. package/src/interactive2/wrapped-defaults.js +86 -0
  210. package/src/interactive2/wrapped-ellipse.js +87 -0
  211. package/src/interactive2/wrapped-line.js +109 -0
  212. package/src/interactive2/wrapped-path.js +47 -0
  213. package/src/interactive2.js +91 -0
  214. package/src/item-renderer.jsx +518 -0
  215. package/src/item-version.js +12 -0
  216. package/src/jipt-paragraphs.js +44 -0
  217. package/src/jquery.mobile.vmouse.js +545 -0
  218. package/src/keypad-context.js +33 -0
  219. package/src/loading-context.js +20 -0
  220. package/src/logging/log.js +107 -0
  221. package/src/logging/log_test.js +41 -0
  222. package/src/mixins/changeable.jsx +115 -0
  223. package/src/mixins/editor-jsonify.jsx +13 -0
  224. package/src/mixins/provide-keypad.jsx +129 -0
  225. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  226. package/src/mixins/widget-prop-blacklist.js +36 -0
  227. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  228. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  229. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
  230. package/src/multi-items/__tests__/items_test.js +245 -0
  231. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  232. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  233. package/src/multi-items/__tests__/shapes_test.js +44 -0
  234. package/src/multi-items/__tests__/trees_test.js +316 -0
  235. package/src/multi-items/item-types.js +55 -0
  236. package/src/multi-items/items.js +176 -0
  237. package/src/multi-items/multi-renderer.jsx +543 -0
  238. package/src/multi-items/prop-type-builders.js +105 -0
  239. package/src/multi-items/shape-types.js +47 -0
  240. package/src/multi-items/shapes.js +53 -0
  241. package/src/multi-items/tree-types.js +42 -0
  242. package/src/multi-items/trees.js +345 -0
  243. package/src/multi-items.js +76 -0
  244. package/src/not-gorgon.js +129 -0
  245. package/src/perseus-api.jsx +270 -0
  246. package/src/perseus-env.js +26 -0
  247. package/src/perseus-error.js +19 -0
  248. package/src/perseus-markdown.jsx +433 -0
  249. package/src/perseus-types.js +1597 -0
  250. package/src/pure-markdown.js +347 -0
  251. package/src/question-paragraph.jsx +30 -0
  252. package/src/renderability.jsx +108 -0
  253. package/src/renderer.jsx +1932 -0
  254. package/src/server-item-renderer.jsx +467 -0
  255. package/src/sigfigs.jsx +492 -0
  256. package/src/styles/articles.less +748 -0
  257. package/src/styles/constants.js +119 -0
  258. package/src/styles/global-constants.js +350 -0
  259. package/src/styles/global-styles.js +307 -0
  260. package/src/styles/khan-exercise.css +1304 -0
  261. package/src/styles/media-queries.js +48 -0
  262. package/src/styles/perseus-renderer.less +1007 -0
  263. package/src/styles/shared.js +132 -0
  264. package/src/styles/util.less +47 -0
  265. package/src/styles/variables.boostrap.less +301 -0
  266. package/src/styles/variables.less +68 -0
  267. package/src/styles/variables.shared.less +356 -0
  268. package/src/styles/widgets/categorizer.less +39 -0
  269. package/src/styles/widgets/definition.less +86 -0
  270. package/src/styles/widgets/dropdown.less +3 -0
  271. package/src/styles/widgets/expression.less +147 -0
  272. package/src/styles/widgets/graded-group.less +36 -0
  273. package/src/styles/widgets/grapher.less +11 -0
  274. package/src/styles/widgets/group.less +19 -0
  275. package/src/styles/widgets/image.less +29 -0
  276. package/src/styles/widgets/interactive-graph.less +61 -0
  277. package/src/styles/widgets/label-image.less +41 -0
  278. package/src/styles/widgets/matcher.less +8 -0
  279. package/src/styles/widgets/matrix.less +120 -0
  280. package/src/styles/widgets/measurer.less +20 -0
  281. package/src/styles/widgets/orderer.less +97 -0
  282. package/src/styles/widgets/passage.less +151 -0
  283. package/src/styles/widgets/plotter.less +33 -0
  284. package/src/styles/widgets/radio.less +52 -0
  285. package/src/styles/widgets/reaction-diagram.less +35 -0
  286. package/src/styles/widgets/sequence.less +5 -0
  287. package/src/styles/widgets/sortable.less +127 -0
  288. package/src/styles/widgets/table.less +56 -0
  289. package/src/styles/widgets/transformer.less +28 -0
  290. package/src/styles/widgets/unit.less +14 -0
  291. package/src/tex-wrangler.js +176 -0
  292. package/src/traversal.jsx +141 -0
  293. package/src/types.js +538 -0
  294. package/src/util/a11y.js +24 -0
  295. package/src/util/answer-types.js +862 -0
  296. package/src/util/answer-types_test.js +115 -0
  297. package/src/util/colors.js +94 -0
  298. package/src/util/fix-passage-refs.jsx +122 -0
  299. package/src/util/graph-utils.js +16 -0
  300. package/src/util/graphie.js +1586 -0
  301. package/src/util/interactive.js +4078 -0
  302. package/src/util/katex-preprocess.js +16 -0
  303. package/src/util/math.js +296 -0
  304. package/src/util/mobile-native-utils.js +14 -0
  305. package/src/util/mobile-native-utils_test.js +24 -0
  306. package/src/util/register-all-widgets-for-testing.js +12 -0
  307. package/src/util/scroll-utils.js +162 -0
  308. package/src/util/sizing-utils.js +46 -0
  309. package/src/util/tex.js +203 -0
  310. package/src/util/url-utils.js +36 -0
  311. package/src/util.js +990 -0
  312. package/src/version.js +11 -0
  313. package/src/widget-container.jsx +174 -0
  314. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  315. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  316. package/src/widgets/__stories__/definition.stories.jsx +38 -0
  317. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  318. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  319. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  320. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  321. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  322. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  323. package/src/widgets/__stories__/group.stories.jsx +15 -0
  324. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  325. package/src/widgets/__stories__/image.stories.jsx +62 -0
  326. package/src/widgets/__stories__/input-number.stories.jsx +90 -0
  327. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  328. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  329. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  330. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  331. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  332. package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
  333. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  334. package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
  335. package/src/widgets/__stories__/passage.stories.jsx +19 -0
  336. package/src/widgets/__stories__/radio.stories.jsx +82 -0
  337. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  338. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  339. package/src/widgets/__stories__/unit.stories.jsx +45 -0
  340. package/src/widgets/__stories__/video-transcript-link.stories.jsx +24 -0
  341. package/src/widgets/__stories__/video.stories.jsx +15 -0
  342. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  343. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  344. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  345. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  346. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  347. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  348. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  349. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  350. package/src/widgets/__testdata__/group_testdata.js +156 -0
  351. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  352. package/src/widgets/__testdata__/image_testdata.js +38 -0
  353. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  354. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  355. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  356. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  357. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  358. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  359. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  360. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  361. package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
  362. package/src/widgets/__testdata__/passage_testdata.js +71 -0
  363. package/src/widgets/__testdata__/radio_testdata.js +210 -0
  364. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  365. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  366. package/src/widgets/__testdata__/video_testdata.js +22 -0
  367. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
  368. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  369. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
  370. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
  371. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  372. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  373. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
  374. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
  375. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
  376. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
  377. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
  378. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  379. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
  380. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  381. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  382. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
  383. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  384. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
  385. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  386. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
  387. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  388. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  389. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  390. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
  391. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
  392. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  393. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  394. package/src/widgets/__tests__/definition_test.js +152 -0
  395. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  396. package/src/widgets/__tests__/explanation_test.js +199 -0
  397. package/src/widgets/__tests__/expression_test.jsx +439 -0
  398. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
  399. package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
  400. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  401. package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
  402. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  403. package/src/widgets/__tests__/group_test.jsx +512 -0
  404. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  405. package/src/widgets/__tests__/image_test.jsx +65 -0
  406. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  407. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  408. package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
  409. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  410. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  411. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  412. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  413. package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
  414. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  415. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  416. package/src/widgets/__tests__/passage_test.jsx +133 -0
  417. package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
  418. package/src/widgets/__tests__/radio_test.jsx +651 -0
  419. package/src/widgets/__tests__/renderQuestion.jsx +66 -0
  420. package/src/widgets/__tests__/unit_test.jsx +144 -0
  421. package/src/widgets/__tests__/video_test.jsx +55 -0
  422. package/src/widgets/categorizer.jsx +338 -0
  423. package/src/widgets/cs-program.jsx +227 -0
  424. package/src/widgets/definition.jsx +141 -0
  425. package/src/widgets/dropdown.jsx +143 -0
  426. package/src/widgets/explanation.jsx +315 -0
  427. package/src/widgets/expression.jsx +652 -0
  428. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  429. package/src/widgets/graded-group-set.jsx +285 -0
  430. package/src/widgets/graded-group.jsx +448 -0
  431. package/src/widgets/grapher/util.jsx +806 -0
  432. package/src/widgets/grapher.jsx +663 -0
  433. package/src/widgets/group.jsx +213 -0
  434. package/src/widgets/iframe.jsx +198 -0
  435. package/src/widgets/image.jsx +310 -0
  436. package/src/widgets/input-number.jsx +411 -0
  437. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  438. package/src/widgets/interaction/color-picker.jsx +69 -0
  439. package/src/widgets/interaction/dash-picker.jsx +46 -0
  440. package/src/widgets/interaction/element-container.jsx +97 -0
  441. package/src/widgets/interaction.jsx +742 -0
  442. package/src/widgets/interactive-graph.jsx +2820 -0
  443. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  444. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  445. package/src/widgets/label-image/answer-choices.jsx +311 -0
  446. package/src/widgets/label-image/marker.jsx +365 -0
  447. package/src/widgets/label-image/types.js +33 -0
  448. package/src/widgets/label-image.jsx +841 -0
  449. package/src/widgets/label-image_test.js +840 -0
  450. package/src/widgets/lights-puzzle.jsx +356 -0
  451. package/src/widgets/matcher.jsx +271 -0
  452. package/src/widgets/matrix.jsx +616 -0
  453. package/src/widgets/measurer.jsx +215 -0
  454. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  455. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  456. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  457. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  458. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  459. package/src/widgets/molecule.jsx +172 -0
  460. package/src/widgets/number-line.jsx +858 -0
  461. package/src/widgets/numeric-input.jsx +622 -0
  462. package/src/widgets/orderer.jsx +753 -0
  463. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
  464. package/src/widgets/passage/get-line-height-for-node.js +10 -0
  465. package/src/widgets/passage/passage-markdown.jsx +416 -0
  466. package/src/widgets/passage-ref-target.jsx +70 -0
  467. package/src/widgets/passage-ref.jsx +198 -0
  468. package/src/widgets/passage.jsx +601 -0
  469. package/src/widgets/plotter.jsx +1198 -0
  470. package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
  471. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
  472. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
  473. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
  474. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
  475. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
  476. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
  477. package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
  478. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
  479. package/src/widgets/radio/base-radio.jsx +642 -0
  480. package/src/widgets/radio/choice-icon.jsx +468 -0
  481. package/src/widgets/radio/choice-none-above.jsx +54 -0
  482. package/src/widgets/radio/choice.jsx +692 -0
  483. package/src/widgets/radio/choice.jsx.fixture.js +64 -0
  484. package/src/widgets/radio/cross-out-button.jsx +136 -0
  485. package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
  486. package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
  487. package/src/widgets/radio/focus-ring.jsx +51 -0
  488. package/src/widgets/radio/option-status.jsx +89 -0
  489. package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
  490. package/src/widgets/radio/util.js +32 -0
  491. package/src/widgets/radio/util_test.js +19 -0
  492. package/src/widgets/radio/widget.jsx +470 -0
  493. package/src/widgets/radio.jsx +156 -0
  494. package/src/widgets/reaction-diagram.jsx +137 -0
  495. package/src/widgets/sequence.jsx +140 -0
  496. package/src/widgets/simulator.jsx +808 -0
  497. package/src/widgets/sorter.jsx +146 -0
  498. package/src/widgets/table.jsx +394 -0
  499. package/src/widgets/transformer.jsx +2876 -0
  500. package/src/widgets/unit.jsx +372 -0
  501. package/src/widgets/video-transcript-link.jsx +84 -0
  502. package/src/widgets/video-transcript-link_test.js +211 -0
  503. package/src/widgets/video.jsx +126 -0
  504. package/src/widgets.js +431 -0
  505. package/src/zoom.js +539 -0
  506. package/README.md +0 -3
  507. package/src/__tests__/index.test.js +0 -14
@@ -0,0 +1,892 @@
1
+ // @flow
2
+
3
+ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
4
+ import {fireEvent, render, screen} from "@testing-library/react";
5
+ import userEvent from "@testing-library/user-event";
6
+ import * as React from "react";
7
+ import "@testing-library/jest-dom"; // Imports custom mathers
8
+
9
+ import {testDependencies} from "../../../../testing/test-dependencies.js";
10
+ import {
11
+ itemWithInput,
12
+ labelImageItem,
13
+ mockedItem,
14
+ } from "../__testdata__/item-renderer_testdata.js";
15
+ import * as Dependencies from "../dependencies.js";
16
+ import ItemRenderer from "../item-renderer.jsx";
17
+ import {registerWidget} from "../widgets.js";
18
+ import {multiChoiceQuestion} from "../widgets/__testdata__/radio_testdata.js";
19
+ import DefinitionExport from "../widgets/definition.jsx";
20
+ import InputNumberExport from "../widgets/input-number.jsx";
21
+ import LabelImageExport from "../widgets/label-image.jsx";
22
+ import RadioWidgetExport from "../widgets/radio.jsx";
23
+
24
+ import MockWidgetExport from "./mock-widget.jsx";
25
+
26
+ import type {PerseusItem, PerseusRenderer} from "../perseus-types.js";
27
+ import type {Widget as WidgetInstance} from "../renderer.jsx";
28
+ import type {APIOptions, PerseusScore} from "../types.js";
29
+
30
+ const Peripherals = () => (
31
+ <div className="test_peripherals">
32
+ <div
33
+ id="calculator"
34
+ data-test-id="calculator"
35
+ style={{display: "none"}}
36
+ />
37
+ <div
38
+ className="periodic-table-info-box"
39
+ data-test-id="periodic-table-info-box"
40
+ style={{display: "none"}}
41
+ />
42
+ <div
43
+ className="z-table-info-box"
44
+ data-test-id="z-table-info-box"
45
+ style={{display: "none"}}
46
+ />
47
+ <div
48
+ className="t-table-info-box"
49
+ data-test-id="t-table-info-box"
50
+ style={{display: "none"}}
51
+ />
52
+ <div
53
+ className="chi2-table-info-box"
54
+ data-test-id="chi2-table-info-box"
55
+ style={{display: "none"}}
56
+ />
57
+ </div>
58
+ );
59
+
60
+ // This looks alot like `widgets/__tests__/renderQuestion.jsx', except we use
61
+ // the ItemRenderer instead of Renderer
62
+ export const renderQuestion = (
63
+ question: PerseusItem,
64
+ apiOptions: APIOptions = Object.freeze({}),
65
+ optionalProps: $Shape<ItemRenderer["props"]> = Object.freeze({}),
66
+ ): {|
67
+ container: HTMLElement,
68
+ renderer: ItemRenderer,
69
+ |} => {
70
+ let renderer: ItemRenderer | null = null;
71
+ const {container} = render(
72
+ <RenderStateRoot>
73
+ <ItemRenderer
74
+ ref={(node) => (renderer = node)}
75
+ apiOptions={apiOptions}
76
+ item={question}
77
+ problemNum={0}
78
+ reviewMode={false}
79
+ savedState=""
80
+ controlPeripherals={false}
81
+ {...optionalProps}
82
+ />
83
+ {/* The ItemRenderer _requires_ two divs: a work area and hints
84
+ area. Without both of these, it fails to render anything! */}
85
+ <div id="workarea" />
86
+ <div id="hintsarea" />
87
+
88
+ <Peripherals />
89
+ </RenderStateRoot>,
90
+ );
91
+ if (!renderer) {
92
+ throw new Error(`Failed to render!`);
93
+ }
94
+ return {container, renderer};
95
+ };
96
+
97
+ // Finds a widget in the given renderer by ID. The return type is non-optional
98
+ // (ie. it assumes the widget will exist) to make Flow happier in the tests
99
+ // that use this function.
100
+ const getWidgetById = (
101
+ renderer: ItemRenderer,
102
+ widgetId: string,
103
+ ): WidgetInstance => {
104
+ const mockWidget = renderer.questionRenderer.getWidgetInstance(widgetId);
105
+ expect(mockWidget).not.toBeUndefined();
106
+ if (mockWidget == null) {
107
+ throw new Error("Oops");
108
+ }
109
+ return mockWidget;
110
+ };
111
+
112
+ const makeHint = (content: string): PerseusRenderer => ({
113
+ content:
114
+ "Maybe if you try using a calculator? See: [[\u2603 mock-hint-widget 1]]",
115
+ widgets: {
116
+ // $FlowFixMe[incompatible-return]
117
+ "mock-hint-widget 1": {
118
+ graded: true,
119
+ version: {major: 0, minor: 0},
120
+ static: false,
121
+ type: "mock-widget",
122
+ options: {static: false},
123
+ alignment: "default",
124
+ },
125
+ },
126
+ images: {},
127
+ });
128
+
129
+ describe("item renderer", () => {
130
+ beforeAll(() => {
131
+ registerWidget("definition", DefinitionExport);
132
+ registerWidget("input-number", InputNumberExport);
133
+ registerWidget("mock-widget", MockWidgetExport);
134
+ });
135
+
136
+ beforeEach(() => {
137
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
138
+ testDependencies,
139
+ );
140
+ });
141
+
142
+ it("should snapshot", () => {
143
+ // Arrange and Act
144
+ const {container} = renderQuestion(itemWithInput);
145
+
146
+ // Assert
147
+ expect(container).toMatchSnapshot("initial render");
148
+ });
149
+
150
+ it("should render the content", () => {
151
+ // Arrange and Act
152
+ renderQuestion(itemWithInput);
153
+
154
+ // Assert
155
+ expect(screen.getByRole("textbox")).toBeVisible();
156
+ });
157
+
158
+ describe("API options callbacks", () => {
159
+ it("should call the answerable callback when mounted", () => {
160
+ // Arrange
161
+ const answerableCallback = jest.fn();
162
+
163
+ // Act
164
+ renderQuestion(itemWithInput, {
165
+ answerableCallback,
166
+ });
167
+
168
+ // Assert
169
+ expect(answerableCallback).toHaveBeenCalledWith(
170
+ false /* Answer is not answerable yet */,
171
+ );
172
+ });
173
+
174
+ it("should call interactionCallback when widget interation happens", () => {
175
+ // Arrange
176
+ const interactionCallback = jest.fn();
177
+ const {renderer} = renderQuestion(mockedItem, {
178
+ interactionCallback,
179
+ });
180
+
181
+ // Act
182
+ renderer.handleInteractWithWidget("mock-widget 3");
183
+
184
+ // Assert
185
+ expect(interactionCallback).toHaveBeenCalled();
186
+ });
187
+ });
188
+
189
+ describe("required DOM elements", () => {
190
+ it("should render empty div if no work area found", () => {
191
+ // Arrange and Act
192
+ renderQuestion(itemWithInput, undefined, {
193
+ workAreaSelector: "non_existant_id",
194
+ });
195
+
196
+ // Assert
197
+ expect(
198
+ screen.queryByText(/Enter the number/),
199
+ ).not.toBeInTheDocument();
200
+ });
201
+
202
+ it("should render empty div if no hints area found", () => {
203
+ // Arrange and Act
204
+ renderQuestion(itemWithInput, undefined, {
205
+ hintsAreaSelector: "non_existant_id",
206
+ });
207
+
208
+ // Assert
209
+ expect(
210
+ screen.queryByText(/Enter the number/),
211
+ ).not.toBeInTheDocument();
212
+ });
213
+ });
214
+
215
+ describe("focus handling", () => {
216
+ it("should focus the first widget who's focus() returns true", () => {
217
+ // Arrange
218
+ const {renderer} = renderQuestion(mockedItem);
219
+ getWidgetById(renderer, "mock-widget 3").focus = jest
220
+ .fn()
221
+ .mockReturnValue(true);
222
+
223
+ // Act
224
+ const didFocus = renderer.focus();
225
+
226
+ // Assert
227
+ expect(didFocus).toBeTrue();
228
+ expect(renderer._currentFocus).toStrictEqual(["mock-widget 3"]);
229
+ });
230
+
231
+ it("should blur the currently focused widget when blur() called", () => {
232
+ // Arrange
233
+ const {renderer} = renderQuestion(itemWithInput);
234
+
235
+ // Set focus to the widget and make sure it worked
236
+ screen.getByRole("textbox").focus();
237
+ expect(renderer._currentFocus).toStrictEqual(["input-number 1"]);
238
+
239
+ // Act
240
+ renderer.blur();
241
+ // There's two async calls (in a series) on the blur path:
242
+ // * _.defer() in renderer.jsx (in _onWidgetBlur())
243
+ // * setTimeout() in item-renderer.jsx (in _onRendererBlur())
244
+ jest.runOnlyPendingTimers();
245
+ jest.runOnlyPendingTimers();
246
+
247
+ // Assert
248
+ expect(renderer._currentFocus).toStrictEqual(null);
249
+ });
250
+
251
+ it("should call the onFocusChange callback when focus set", () => {
252
+ // Arrange
253
+ const onFocusChange = jest.fn();
254
+
255
+ const {renderer} = renderQuestion(mockedItem, {
256
+ onFocusChange,
257
+ });
258
+ getWidgetById(renderer, "mock-widget 2").focus = jest
259
+ .fn()
260
+ .mockReturnValue(true);
261
+
262
+ // Act
263
+ renderer.focus();
264
+
265
+ // Assert
266
+ expect(onFocusChange).toHaveBeenCalledWith(
267
+ ["mock-widget 2"], // widget that received focus
268
+ null, // previously focused widget
269
+ false, // did focus the input (our mock widget doesn't have input)
270
+ );
271
+ });
272
+
273
+ it("should provide previous focus widgetId to onFocusChange", () => {
274
+ // Arrange
275
+ const onFocusChange = jest.fn();
276
+
277
+ const {renderer} = renderQuestion(mockedItem, {
278
+ onFocusChange,
279
+ });
280
+
281
+ getWidgetById(renderer, "mock-widget 4").focus = jest
282
+ .fn()
283
+ .mockReturnValue(true);
284
+ renderer.focus();
285
+
286
+ getWidgetById(renderer, "mock-widget 2").focus = jest
287
+ .fn()
288
+ .mockReturnValue(true);
289
+
290
+ // Act
291
+ const gotFocus = renderer.focus();
292
+
293
+ // Assert
294
+ expect(gotFocus).toBeTrue();
295
+ expect(renderer._currentFocus).toStrictEqual(["mock-widget 2"]);
296
+ expect(onFocusChange).toHaveBeenCalledWith(
297
+ ["mock-widget 2"], // widget that received focus
298
+ ["mock-widget 4"], // previously focused widget
299
+ false, // did focus the input (our mock widget doesn't have input)
300
+ );
301
+ });
302
+
303
+ it("should activate the keypad when widget with input is focused", () => {
304
+ // Arrange
305
+ const {renderer} = renderQuestion(itemWithInput, {
306
+ isMobile: true,
307
+ customKeypad: true,
308
+ });
309
+
310
+ // Act
311
+ renderer.focus();
312
+
313
+ // Assert
314
+ expect(screen.getByLabelText("Up arrow")).toBeVisible();
315
+ });
316
+
317
+ it("should provide current and previous focus paths on focus change to, and away from, a single widget", () => {
318
+ // Arrange
319
+ const onFocusChange = jest.fn();
320
+ renderQuestion(itemWithInput, {
321
+ onFocusChange,
322
+ });
323
+
324
+ userEvent.click(screen.getByRole("textbox"));
325
+
326
+ // Act
327
+ userEvent.tab();
328
+ // There's two async calls (in a series) on the blur path:
329
+ // * _.defer() in renderer.jsx (in _onWidgetBlur())
330
+ // * setTimeout() in item-renderer.jsx (in _onRendererBlur())
331
+ jest.runOnlyPendingTimers();
332
+ jest.runOnlyPendingTimers();
333
+
334
+ // Assert
335
+ expect(onFocusChange).toHaveBeenCalledTimes(2);
336
+ expect(onFocusChange).toHaveBeenNthCalledWith(
337
+ 1,
338
+ ["input-number 1"], // current focus
339
+ null, // previous focus
340
+ null, // no keypad element?
341
+ );
342
+ expect(onFocusChange).toHaveBeenNthCalledWith(
343
+ 2,
344
+ null, // current focus
345
+ ["input-number 1"], // previous focus
346
+ null, // no keypad element?
347
+ );
348
+ });
349
+
350
+ it("should provide current and previous focus paths on focus change from one widget to another", () => {
351
+ // Arrange
352
+ const onFocusChange = jest.fn();
353
+ const inputNumber =
354
+ itemWithInput.question.widgets["input-number 1"];
355
+
356
+ renderQuestion(
357
+ {
358
+ ...itemWithInput,
359
+ question: {
360
+ ...itemWithInput.question,
361
+ content:
362
+ "Enter the number $$-42$$ in both boxes: \n\n[[\u2603 input-number 1]]\n\n[[\u2603 input-number 2]]",
363
+ widgets: {
364
+ "input-number 1": inputNumber,
365
+ "input-number 2": inputNumber,
366
+ },
367
+ },
368
+ },
369
+ {onFocusChange},
370
+ );
371
+
372
+ // Focus second input to start
373
+ userEvent.click(screen.queryAllByRole("textbox")[1]);
374
+
375
+ // Act
376
+ userEvent.click(screen.queryAllByRole("textbox")[0]);
377
+ // There's two async calls (in a series) on the blur path:
378
+ // * _.defer() in renderer.jsx (in _onWidgetBlur())
379
+ // * setTimeout() in item-renderer.jsx (in _onRendererBlur())
380
+ jest.runOnlyPendingTimers();
381
+ jest.runOnlyPendingTimers();
382
+
383
+ // Assert
384
+ expect(onFocusChange).toHaveBeenCalledTimes(2);
385
+ expect(onFocusChange).toHaveBeenNthCalledWith(
386
+ 1,
387
+ ["input-number 2"], // current focus
388
+ null, // previous focus
389
+ null, // no keypad element?
390
+ );
391
+ expect(onFocusChange).toHaveBeenNthCalledWith(
392
+ 2,
393
+ ["input-number 1"], // current focus
394
+ ["input-number 2"], // previous focus
395
+ null, // no keypad element?
396
+ );
397
+ });
398
+
399
+ it("should set given value to widget at focus path", () => {
400
+ // Arrange
401
+ const {renderer} = renderQuestion(itemWithInput);
402
+ const cb = jest.fn();
403
+
404
+ // Act
405
+ renderer.setInputValue(["input-number 1"], "answer_42", cb);
406
+ jest.runOnlyPendingTimers(); // due to setState()
407
+
408
+ // Assert
409
+ expect(cb).toHaveBeenCalled();
410
+ expect(screen.getByDisplayValue("answer_42")).toBeVisible();
411
+ });
412
+
413
+ it("should focus the widget found at the given path", () => {
414
+ // Arrange
415
+ const {renderer} = renderQuestion(itemWithInput);
416
+
417
+ // Act
418
+ renderer.focusPath(["input-number 1"]);
419
+
420
+ // Assert
421
+ expect(screen.getByRole("textbox")).toHaveFocus();
422
+ });
423
+
424
+ it("should blur the widget found at the given path", () => {
425
+ // Arrange
426
+ const {renderer} = renderQuestion(itemWithInput);
427
+ screen.getByRole("textbox").focus();
428
+
429
+ // Act
430
+ renderer.blurPath(["input-number 1"]);
431
+ // There's two async calls (in a series) on the blur path:
432
+ // * _.defer() in renderer.jsx (in _onWidgetBlur())
433
+ // * setTimeout() in item-renderer.jsx (in _onRendererBlur())
434
+ jest.runOnlyPendingTimers();
435
+ jest.runOnlyPendingTimers();
436
+
437
+ // Assert
438
+ expect(screen.getByRole("textbox")).not.toHaveFocus();
439
+ });
440
+
441
+ // TODO(jeremy): I couldn't get this test to work, but I'd like to eventually
442
+ it.skip("should dismiss the keypad if no input element focused (mobile)", () => {
443
+ // Arrange
444
+ const onFocusChange = jest.fn();
445
+ renderQuestion(itemWithInput, {
446
+ onFocusChange,
447
+ isMobile: true,
448
+ customKeypad: true,
449
+ });
450
+
451
+ const mathInput = screen.getByRole("textbox");
452
+ fireEvent.touchStart(mathInput);
453
+ fireEvent.touchEnd(mathInput);
454
+ jest.runOnlyPendingTimers();
455
+ // Make sure the keypad appeared
456
+ expect(screen.getByLabelText("Up arrow")).toBeVisible();
457
+
458
+ // Act
459
+ const instructions = screen.getByText(/Enter the number/);
460
+ fireEvent.touchStart(instructions);
461
+ fireEvent.touchEnd(instructions);
462
+ // There's two defer()'s on the blur path:
463
+ // * one in renderer.jsx
464
+ // * one in item-renderer.jsx
465
+ jest.runOnlyPendingTimers();
466
+ jest.runOnlyPendingTimers();
467
+
468
+ // Assert
469
+ // TODO!!
470
+ expect(screen.getByLabelText("Up arrow")).not.toBeVisible();
471
+ });
472
+ });
473
+
474
+ describe("scoring", () => {
475
+ it("should highlight empty widgets when scoring", () => {
476
+ // Arrange
477
+ const {renderer} = renderQuestion(itemWithInput);
478
+
479
+ // Act
480
+ const score = renderer.scoreInput();
481
+ jest.runOnlyPendingTimers(); // due to setState()
482
+
483
+ // Assert
484
+ expect(score.empty).toBeTrue();
485
+ expect(screen.getByRole("textbox")).toBeHighlighted();
486
+ });
487
+
488
+ it("should remove highlight after marking widget as having had interaction", () => {
489
+ // Arrange
490
+ const {renderer} = renderQuestion(itemWithInput);
491
+ renderer.scoreInput(); // Triggers highlighting
492
+ expect(screen.getByRole("textbox")).toBeHighlighted();
493
+
494
+ // Act
495
+ screen.getByRole("textbox").focus();
496
+ userEvent.keyboard("23");
497
+ jest.runOnlyPendingTimers(); // due to setState()
498
+
499
+ // Assert
500
+ expect(screen.getByRole("textbox")).not.toBeHighlighted();
501
+ });
502
+
503
+ it("should return a score for each widget when scoreWidgets() called", () => {
504
+ // Arrange
505
+ const correct: PerseusScore = {earned: 1, total: 1, type: "points"};
506
+ const wrong: PerseusScore = {
507
+ type: "invalid",
508
+ message: "please enter a value",
509
+ };
510
+
511
+ let isCorrect = true;
512
+ const ticktock = (): PerseusScore => {
513
+ const wasCorrect = isCorrect;
514
+ isCorrect = !isCorrect;
515
+ if (wasCorrect) {
516
+ return correct;
517
+ }
518
+ return wrong;
519
+ };
520
+
521
+ const {renderer} = renderQuestion(mockedItem);
522
+ for (const [id] of Object.entries(mockedItem.question.widgets)) {
523
+ const widget = getWidgetById(renderer, id);
524
+ widget.simpleValidate = jest.fn().mockReturnValue(ticktock());
525
+ widget.getUserInput = jest
526
+ .fn()
527
+ .mockReturnValue(`user input for ${id}`);
528
+ widget.getSerializedState = jest
529
+ .fn()
530
+ .mockReturnValue({id, a: 1, b: "state"});
531
+ }
532
+
533
+ // Act
534
+ const scores = renderer.scoreWidgets();
535
+
536
+ // Assert
537
+ expect(scores).toMatchInlineSnapshot(`
538
+ Object {
539
+ "mock-widget 1": Object {
540
+ "correct": true,
541
+ "empty": false,
542
+ "guess": "user input for mock-widget 1",
543
+ "message": undefined,
544
+ "state": Object {
545
+ "a": 1,
546
+ "b": "state",
547
+ "id": "mock-widget 1",
548
+ },
549
+ },
550
+ "mock-widget 2": Object {
551
+ "correct": false,
552
+ "empty": true,
553
+ "guess": "user input for mock-widget 2",
554
+ "message": "please enter a value",
555
+ "state": Object {
556
+ "a": 1,
557
+ "b": "state",
558
+ "id": "mock-widget 2",
559
+ },
560
+ "suppressAlmostThere": undefined,
561
+ },
562
+ "mock-widget 3": Object {
563
+ "correct": true,
564
+ "empty": false,
565
+ "guess": "user input for mock-widget 3",
566
+ "message": undefined,
567
+ "state": Object {
568
+ "a": 1,
569
+ "b": "state",
570
+ "id": "mock-widget 3",
571
+ },
572
+ },
573
+ "mock-widget 4": Object {
574
+ "correct": false,
575
+ "empty": true,
576
+ "guess": "user input for mock-widget 4",
577
+ "message": "please enter a value",
578
+ "state": Object {
579
+ "a": 1,
580
+ "b": "state",
581
+ "id": "mock-widget 4",
582
+ },
583
+ "suppressAlmostThere": undefined,
584
+ },
585
+ }
586
+ `);
587
+ });
588
+ });
589
+
590
+ describe("rationales", () => {
591
+ it("should show rationales for widgets that support it", () => {
592
+ // Arrange
593
+ registerWidget("label-image", LabelImageExport);
594
+
595
+ const {renderer} = renderQuestion(labelImageItem);
596
+
597
+ // Act
598
+ renderer.showRationalesForCurrentlySelectedChoices();
599
+
600
+ // Assert
601
+ expect(
602
+ screen.getByText(
603
+ /Click each dot on the image to select an answer/,
604
+ ),
605
+ ).toBeVisible();
606
+ });
607
+
608
+ it("should show not throw if none of the rendered widgets support supporationales", () => {
609
+ // Arrange
610
+ const {renderer} = renderQuestion(itemWithInput);
611
+
612
+ // Act and Assert
613
+ expect(() => {
614
+ renderer.showRationalesForCurrentlySelectedChoices();
615
+ }).not.toThrow();
616
+ });
617
+ });
618
+
619
+ describe("support functions", () => {
620
+ it("should return all widget IDs when getWidgetIds() called", () => {
621
+ // Arrange
622
+ const {renderer} = renderQuestion(mockedItem);
623
+
624
+ // Act
625
+ const ids = renderer.getWidgetIds();
626
+
627
+ // Assert
628
+ expect(ids).toStrictEqual([
629
+ "mock-widget 1",
630
+ "mock-widget 2",
631
+ "mock-widget 3",
632
+ "mock-widget 4",
633
+ ]);
634
+ });
635
+
636
+ it("should return the DOM element for the widget at the given path", () => {
637
+ // Arrange
638
+ const {renderer} = renderQuestion(itemWithInput);
639
+
640
+ // Act
641
+ const node = renderer.getDOMNodeForPath(["input-number 1"]);
642
+
643
+ // Assert
644
+ expect(node).toBeInTheDocument();
645
+ const input = screen.getByRole("textbox");
646
+ expect(node?.contains(input)).toBeTrue();
647
+ });
648
+
649
+ it("should return null when widget not found for the given path", () => {
650
+ // Arrange
651
+ const {renderer} = renderQuestion(itemWithInput);
652
+
653
+ // Act
654
+ const node = renderer.getDOMNodeForPath([
655
+ "this-is-not-a-valid-widget-id",
656
+ ]);
657
+
658
+ // Assert
659
+ expect(node).toBeNull();
660
+ });
661
+
662
+ it("should do grammar type for the widget at the given path", () => {
663
+ // Arrange
664
+ const {renderer} = renderQuestion(mockedItem);
665
+ getWidgetById(renderer, "mock-widget 2").getGrammarTypeForPath =
666
+ jest.fn().mockReturnValue("yabadabadoo!");
667
+
668
+ // Act
669
+ const grammarType = renderer.getGrammarTypeForPath([
670
+ "mock-widget 2",
671
+ ]);
672
+
673
+ // Assert
674
+ expect(grammarType).toBe("yabadabadoo!");
675
+ });
676
+
677
+ it("should return input paths for widgets supporting input", () => {
678
+ // Arrange
679
+ const {renderer} = renderQuestion(mockedItem);
680
+ getWidgetById(renderer, "mock-widget 1").getInputPaths = jest
681
+ .fn()
682
+ .mockReturnValue([["a singular path"]]);
683
+ getWidgetById(renderer, "mock-widget 4").getInputPaths = jest
684
+ .fn()
685
+ .mockReturnValue([["some", "path"]]);
686
+
687
+ // Act
688
+ const inputPaths = renderer.getInputPaths();
689
+
690
+ // Assert
691
+ expect(inputPaths).toStrictEqual([
692
+ ["mock-widget 1", "a singular path"],
693
+ ["mock-widget 4", "some", "path"],
694
+ ]);
695
+ });
696
+
697
+ it("should forward deselectIncorrectSelectedChoices calls to questionRenderer", () => {
698
+ // Arrange
699
+ registerWidget("radio", RadioWidgetExport);
700
+ const {renderer} = renderQuestion({
701
+ ...itemWithInput,
702
+ question: multiChoiceQuestion,
703
+ });
704
+ screen.getAllByRole("checkbox").map((r) => userEvent.click(r));
705
+ jest.runOnlyPendingTimers(); // due to setState()
706
+
707
+ // Act
708
+ renderer.deselectIncorrectSelectedChoices();
709
+
710
+ // Assert
711
+ const checkboxes = screen.getAllByRole("checkbox");
712
+ expect(checkboxes[0]).not.toBeChecked();
713
+ expect(checkboxes[1]).not.toBeChecked();
714
+ expect(checkboxes[2]).not.toBeChecked();
715
+ expect(checkboxes[3]).toBeChecked();
716
+ });
717
+
718
+ it("should return serialized state for each widget and hints", () => {
719
+ // Arrange
720
+ const {renderer} = renderQuestion({
721
+ ...mockedItem,
722
+ hints: [
723
+ makeHint("Maybe if you try using a calculator?"),
724
+ makeHint("I think you need a TI-80 to get this done!"),
725
+ ],
726
+ });
727
+ // We get the result of the widget's getSerializedState() if it
728
+ // implements it, otherwise we just get it's props.
729
+ getWidgetById(renderer, "mock-widget 1").getSerializedState = jest
730
+ .fn()
731
+ .mockReturnValue({id: "mock-widget 1", a: 1, b: "state 1"});
732
+ getWidgetById(renderer, "mock-widget 4").getSerializedState = jest
733
+ .fn()
734
+ .mockReturnValue({id: "mock-widget 4", a: 4, b: "state 4"});
735
+
736
+ renderer.showHint(); // should be one left
737
+
738
+ // Act
739
+ const state = renderer.getSerializedState();
740
+
741
+ // Assert
742
+ expect(state).toStrictEqual({
743
+ hints: [{"mock-hint-widget 1": {static: false}}],
744
+ question: {
745
+ "mock-widget 1": {id: "mock-widget 1", a: 1, b: "state 1"},
746
+ "mock-widget 2": {smiling: false, static: false},
747
+ "mock-widget 3": {smiling: true, static: false},
748
+ "mock-widget 4": {id: "mock-widget 4", a: 4, b: "state 4"},
749
+ },
750
+ });
751
+ });
752
+ });
753
+
754
+ describe("controlPeripherals is disabled", () => {
755
+ const extraProps = {controlPeripherals: false};
756
+
757
+ it("should hide peripherals if answerArea flag(s) are false", () => {
758
+ // Arrange
759
+ const item = {...itemWithInput, answerArea: null};
760
+
761
+ // Act
762
+ renderQuestion(item, undefined, extraProps);
763
+
764
+ // Assert
765
+ expect(screen.getByTestId("calculator")).not.toBeVisible();
766
+ expect(
767
+ screen.getByTestId("periodic-table-info-box"),
768
+ ).not.toBeVisible();
769
+ expect(screen.getByTestId("z-table-info-box")).not.toBeVisible();
770
+ expect(screen.getByTestId("t-table-info-box")).not.toBeVisible();
771
+ expect(screen.getByTestId("chi2-table-info-box")).not.toBeVisible();
772
+ });
773
+
774
+ it("should call onShowXyz peripheral callbacks", () => {
775
+ // Arrange
776
+ const item = {
777
+ ...itemWithInput,
778
+ answerArea: {
779
+ zTable: true,
780
+ chi2Table: true,
781
+ tTable: true,
782
+ calculator: true,
783
+ periodicTable: true,
784
+ },
785
+ };
786
+
787
+ const onShowCalculator = jest.fn();
788
+ const onShowPeriodicTable = jest.fn();
789
+ const onShowZTable = jest.fn();
790
+ const onShowTTable = jest.fn();
791
+ const onShowChi2Table = jest.fn();
792
+ const localExtraProps = {
793
+ ...extraProps,
794
+ onShowCalculator,
795
+ onShowPeriodicTable,
796
+ onShowZTable,
797
+ onShowTTable,
798
+ onShowChi2Table,
799
+ };
800
+
801
+ // Act
802
+ renderQuestion(item, undefined, localExtraProps);
803
+
804
+ // Assert
805
+ expect(onShowCalculator).toHaveBeenCalled();
806
+ expect(onShowPeriodicTable).toHaveBeenCalled();
807
+ expect(onShowZTable).toHaveBeenCalled();
808
+ expect(onShowTTable).toHaveBeenCalled();
809
+ expect(onShowChi2Table).toHaveBeenCalled();
810
+ });
811
+ });
812
+
813
+ describe("controlPeripherals is enabled", () => {
814
+ const extraProps: $Shape<ItemRenderer["props"]> = {
815
+ controlPeripherals: true,
816
+ };
817
+
818
+ it("should show peripherals if answerArea flag(s) are true", () => {
819
+ // Arrange
820
+ const item = {
821
+ ...itemWithInput,
822
+ answerArea: {
823
+ zTable: true,
824
+ chi2Table: true,
825
+ tTable: true,
826
+ calculator: true,
827
+ periodicTable: true,
828
+ },
829
+ };
830
+
831
+ // Act
832
+ renderQuestion(item, undefined, extraProps);
833
+
834
+ // Assert
835
+ expect(screen.getByTestId("calculator")).toBeVisible();
836
+ expect(screen.getByTestId("periodic-table-info-box")).toBeVisible();
837
+ expect(screen.getByTestId("z-table-info-box")).toBeVisible();
838
+ expect(screen.getByTestId("t-table-info-box")).toBeVisible();
839
+ expect(screen.getByTestId("chi2-table-info-box")).toBeVisible();
840
+ });
841
+
842
+ it("should hide peripherals if answerArea flag(s) are false", () => {
843
+ // Arrange
844
+ const item = {
845
+ ...itemWithInput,
846
+ answerArea: {
847
+ zTable: false,
848
+ chi2Table: false,
849
+ tTable: false,
850
+ calculator: false,
851
+ periodicTable: false,
852
+ },
853
+ };
854
+
855
+ // Act
856
+ renderQuestion(item, undefined, extraProps);
857
+
858
+ // Assert
859
+ expect(screen.getByTestId("calculator")).not.toBeVisible();
860
+ expect(
861
+ screen.getByTestId("periodic-table-info-box"),
862
+ ).not.toBeVisible();
863
+ expect(screen.getByTestId("z-table-info-box")).not.toBeVisible();
864
+ expect(screen.getByTestId("t-table-info-box")).not.toBeVisible();
865
+ expect(screen.getByTestId("chi2-table-info-box")).not.toBeVisible();
866
+ });
867
+
868
+ it("should call the setDrawingAreaAvailable callback when mounted", () => {
869
+ // Arrange
870
+ const item = {
871
+ ...itemWithInput,
872
+ answerArea: {
873
+ zTable: false,
874
+ chi2Table: false,
875
+ tTable: false,
876
+ calculator: false,
877
+ periodicTable: false,
878
+ },
879
+ };
880
+ const onDrawingAreaAvailable = jest.fn();
881
+ const apiOptions = {
882
+ setDrawingAreaAvailable: onDrawingAreaAvailable,
883
+ };
884
+
885
+ // Act
886
+ renderQuestion(item, apiOptions, extraProps);
887
+
888
+ // Assert
889
+ expect(onDrawingAreaAvailable).toHaveBeenCalledWith(true);
890
+ });
891
+ });
892
+ });