@pie-lib/config-ui 11.30.3-next.0 → 11.30.3-next.162

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 (120) hide show
  1. package/CHANGELOG.md +19 -67
  2. package/esm/package.json +3 -0
  3. package/lib/alert-dialog.js +35 -42
  4. package/lib/alert-dialog.js.map +1 -1
  5. package/lib/checkbox.js +56 -71
  6. package/lib/checkbox.js.map +1 -1
  7. package/lib/choice-configuration/feedback-menu.js +29 -64
  8. package/lib/choice-configuration/feedback-menu.js.map +1 -1
  9. package/lib/choice-configuration/index.js +202 -262
  10. package/lib/choice-configuration/index.js.map +1 -1
  11. package/lib/choice-utils.js +6 -18
  12. package/lib/choice-utils.js.map +1 -1
  13. package/lib/feedback-config/feedback-selector.js +79 -115
  14. package/lib/feedback-config/feedback-selector.js.map +1 -1
  15. package/lib/feedback-config/group.js +26 -40
  16. package/lib/feedback-config/group.js.map +1 -1
  17. package/lib/feedback-config/index.js +47 -90
  18. package/lib/feedback-config/index.js.map +1 -1
  19. package/lib/form-section.js +31 -33
  20. package/lib/form-section.js.map +1 -1
  21. package/lib/help.js +39 -80
  22. package/lib/help.js.map +1 -1
  23. package/lib/index.js +1 -31
  24. package/lib/index.js.map +1 -1
  25. package/lib/input.js +21 -54
  26. package/lib/input.js.map +1 -1
  27. package/lib/inputs.js +61 -95
  28. package/lib/inputs.js.map +1 -1
  29. package/lib/langs.js +58 -101
  30. package/lib/langs.js.map +1 -1
  31. package/lib/layout/config-layout.js +40 -70
  32. package/lib/layout/config-layout.js.map +1 -1
  33. package/lib/layout/index.js +0 -3
  34. package/lib/layout/index.js.map +1 -1
  35. package/lib/layout/layout-contents.js +72 -103
  36. package/lib/layout/layout-contents.js.map +1 -1
  37. package/lib/layout/settings-box.js +27 -56
  38. package/lib/layout/settings-box.js.map +1 -1
  39. package/lib/mui-box/index.js +41 -57
  40. package/lib/mui-box/index.js.map +1 -1
  41. package/lib/number-text-field-custom.js +79 -161
  42. package/lib/number-text-field-custom.js.map +1 -1
  43. package/lib/number-text-field.js +80 -114
  44. package/lib/number-text-field.js.map +1 -1
  45. package/lib/radio-with-label.js +30 -31
  46. package/lib/radio-with-label.js.map +1 -1
  47. package/lib/settings/display-size.js +16 -32
  48. package/lib/settings/display-size.js.map +1 -1
  49. package/lib/settings/index.js +14 -47
  50. package/lib/settings/index.js.map +1 -1
  51. package/lib/settings/panel.js +159 -229
  52. package/lib/settings/panel.js.map +1 -1
  53. package/lib/settings/settings-radio-label.js +28 -30
  54. package/lib/settings/settings-radio-label.js.map +1 -1
  55. package/lib/settings/toggle.js +35 -46
  56. package/lib/settings/toggle.js.map +1 -1
  57. package/lib/tabs/index.js +22 -57
  58. package/lib/tabs/index.js.map +1 -1
  59. package/lib/tags-input/index.js +50 -99
  60. package/lib/tags-input/index.js.map +1 -1
  61. package/lib/two-choice.js +46 -90
  62. package/lib/two-choice.js.map +1 -1
  63. package/lib/with-stateful-model.js +8 -31
  64. package/lib/with-stateful-model.js.map +1 -1
  65. package/package.json +20 -12
  66. package/src/__tests__/alert-dialog.test.jsx +283 -0
  67. package/src/__tests__/checkbox.test.jsx +249 -0
  68. package/src/__tests__/form-section.test.jsx +334 -0
  69. package/src/__tests__/help.test.jsx +184 -0
  70. package/src/__tests__/input.test.jsx +192 -0
  71. package/src/__tests__/langs.test.jsx +435 -15
  72. package/src/__tests__/number-text-field-custom.test.jsx +438 -0
  73. package/src/__tests__/number-text-field.test.jsx +295 -102
  74. package/src/__tests__/radio-with-label.test.jsx +259 -0
  75. package/src/__tests__/settings-panel.test.js +66 -83
  76. package/src/__tests__/settings.test.jsx +515 -0
  77. package/src/__tests__/tabs.test.jsx +193 -0
  78. package/src/__tests__/two-choice.test.js +104 -18
  79. package/src/__tests__/with-stateful-model.test.jsx +145 -0
  80. package/src/alert-dialog.jsx +21 -19
  81. package/src/checkbox.jsx +42 -46
  82. package/src/choice-configuration/__tests__/feedback-menu.test.jsx +157 -4
  83. package/src/choice-configuration/__tests__/index.test.jsx +198 -56
  84. package/src/choice-configuration/feedback-menu.jsx +6 -6
  85. package/src/choice-configuration/index.jsx +201 -196
  86. package/src/feedback-config/__tests__/feedback-config.test.jsx +130 -60
  87. package/src/feedback-config/__tests__/feedback-selector.test.jsx +87 -40
  88. package/src/feedback-config/feedback-selector.jsx +52 -53
  89. package/src/feedback-config/group.jsx +21 -22
  90. package/src/feedback-config/index.jsx +27 -29
  91. package/src/form-section.jsx +26 -18
  92. package/src/help.jsx +20 -28
  93. package/src/input.jsx +1 -1
  94. package/src/inputs.jsx +34 -50
  95. package/src/langs.jsx +41 -46
  96. package/src/layout/__tests__/config.layout.test.jsx +55 -38
  97. package/src/layout/config-layout.jsx +38 -32
  98. package/src/layout/layout-contents.jsx +38 -39
  99. package/src/layout/settings-box.jsx +16 -19
  100. package/src/mui-box/index.jsx +35 -43
  101. package/src/number-text-field-custom.jsx +30 -36
  102. package/src/number-text-field.jsx +45 -29
  103. package/src/radio-with-label.jsx +25 -13
  104. package/src/settings/display-size.jsx +12 -11
  105. package/src/settings/panel.jsx +97 -91
  106. package/src/settings/settings-radio-label.jsx +25 -13
  107. package/src/settings/toggle.jsx +30 -29
  108. package/src/tabs/index.jsx +8 -8
  109. package/src/tags-input/__tests__/index.test.jsx +88 -37
  110. package/src/tags-input/index.jsx +35 -38
  111. package/src/two-choice.jsx +15 -19
  112. package/src/__tests__/__snapshots__/langs.test.jsx.snap +0 -32
  113. package/src/__tests__/__snapshots__/settings-panel.test.js.snap +0 -115
  114. package/src/__tests__/__snapshots__/two-choice.test.js.snap +0 -171
  115. package/src/choice-configuration/__tests__/__snapshots__/feedback-menu.test.jsx.snap +0 -51
  116. package/src/choice-configuration/__tests__/__snapshots__/index.test.jsx.snap +0 -519
  117. package/src/feedback-config/__tests__/__snapshots__/feedback-config.test.jsx.snap +0 -27
  118. package/src/feedback-config/__tests__/__snapshots__/feedback-selector.test.jsx.snap +0 -38
  119. package/src/layout/__tests__/__snapshots__/config.layout.test.jsx.snap +0 -59
  120. package/src/tags-input/__tests__/__snapshots__/index.test.jsx.snap +0 -170
@@ -1,117 +1,177 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { withStyles } from '@material-ui/core/styles';
4
- import TextField from '@material-ui/core/TextField';
5
- import ActionDelete from '@material-ui/icons/Delete';
6
- import ArrowRight from '@material-ui/icons/SubdirectoryArrowRight';
7
- import IconButton from '@material-ui/core/IconButton';
8
- import classNames from 'classnames';
3
+ import { styled } from '@mui/material/styles';
4
+ import TextField from '@mui/material/TextField';
5
+ import ActionDelete from '@mui/icons-material/Delete';
6
+ import ArrowRight from '@mui/icons-material/SubdirectoryArrowRight';
7
+ import IconButton from '@mui/material/IconButton';
9
8
  import { InputContainer } from '@pie-lib/render-ui';
10
- import EditableHtml from '@pie-lib/editable-html';
9
+ // import EditableHtml from '@pie-lib/editable-html';
11
10
  import { InputCheckbox, InputRadio } from '../inputs';
12
11
  import FeedbackMenu from './feedback-menu';
13
12
 
14
- const EditableHtmlContainer = withStyles((theme) => ({
15
- labelContainer: {},
16
- editorHolder: {
17
- marginTop: theme.spacing.unit * 2,
18
- },
19
- }))(
20
- ({
21
- label,
22
- classes,
23
- onChange,
24
- value,
25
- className,
26
- imageSupport,
27
- disableImageAlignmentButtons,
28
- disabled,
29
- spellCheck,
30
- nonEmpty,
31
- pluginOpts,
32
- toolbarOpts,
33
- error,
34
- maxImageWidth,
35
- maxImageHeight,
36
- uploadSoundSupport,
37
- mathMlOptions = {},
38
- }) => {
39
- const names = classNames(classes.labelContainer, className);
13
+ // - mathquill error window not defined
14
+ let EditableHtml;
15
+ if (typeof window !== 'undefined') {
16
+ EditableHtml = require('@pie-lib/editable-html')['default'];
17
+ }
40
18
 
41
- return (
42
- <InputContainer label={label} className={names}>
43
- <div className={classes.editorHolder}>
44
- <EditableHtml
45
- markup={value || ''}
46
- disabled={disabled}
47
- spellCheck={spellCheck}
48
- nonEmpty={nonEmpty}
49
- onChange={onChange}
50
- imageSupport={imageSupport}
51
- disableImageAlignmentButtons={disableImageAlignmentButtons}
52
- className={classes.editor}
53
- pluginProps={pluginOpts || {}}
54
- toolbarOpts={toolbarOpts}
55
- error={error}
56
- maxImageWidth={maxImageWidth}
57
- maxImageHeight={maxImageHeight}
58
- uploadSoundSupport={uploadSoundSupport}
59
- languageCharactersProps={[{ language: 'spanish' }, { language: 'special' }]}
60
- mathMlOptions={mathMlOptions}
61
- />
62
- </div>
63
- </InputContainer>
64
- );
65
- },
66
- );
67
-
68
- const Feedback = withStyles((theme) => ({
69
- text: {
70
- width: '100%',
71
- },
72
- feedbackContainer: {
73
- position: 'relative',
74
- },
75
- arrowIcon: {
76
- fill: theme.palette.grey[400],
77
- left: -56,
78
- position: 'absolute',
79
- top: 20,
80
- },
81
- }))(({ value, onChange, type, correct, classes, defaults, toolbarOpts, mathMlOptions = {} }) => {
19
+ const StyledEditorHolder = styled('div')(({ theme }) => ({
20
+ marginTop: theme.spacing(2),
21
+ }));
22
+
23
+ const EditableHtmlContainer = ({
24
+ label,
25
+ onChange,
26
+ value,
27
+ className,
28
+ imageSupport,
29
+ disableImageAlignmentButtons,
30
+ disabled,
31
+ spellCheck,
32
+ nonEmpty,
33
+ pluginOpts,
34
+ toolbarOpts,
35
+ error,
36
+ maxImageWidth,
37
+ maxImageHeight,
38
+ uploadSoundSupport,
39
+ mathMlOptions = {},
40
+ }) => {
41
+
42
+ return (
43
+ <InputContainer label={label} className={className}>
44
+ <StyledEditorHolder>
45
+ <EditableHtml
46
+ markup={value || ''}
47
+ disabled={disabled}
48
+ spellCheck={spellCheck}
49
+ nonEmpty={nonEmpty}
50
+ onChange={onChange}
51
+ imageSupport={imageSupport}
52
+ disableImageAlignmentButtons={disableImageAlignmentButtons}
53
+ pluginProps={pluginOpts || {}}
54
+ toolbarOpts={toolbarOpts}
55
+ error={error}
56
+ maxImageWidth={maxImageWidth}
57
+ maxImageHeight={maxImageHeight}
58
+ uploadSoundSupport={uploadSoundSupport}
59
+ languageCharactersProps={[{ language: 'spanish' }, { language: 'special' }]}
60
+ mathMlOptions={mathMlOptions}
61
+ />
62
+ </StyledEditorHolder>
63
+ </InputContainer>
64
+ );
65
+ };
66
+
67
+ const StyledFeedbackContainer = styled('div')(() => ({
68
+ position: 'relative',
69
+ }));
70
+
71
+ const StyledArrowIcon = styled(ArrowRight)(({ theme }) => ({
72
+ fill: theme.palette.grey[400],
73
+ left: -56,
74
+ position: 'absolute',
75
+ top: 40,
76
+ }));
77
+
78
+ const StyledTextField = styled(TextField)(({ theme }) => ({
79
+ width: '100%',
80
+ marginTop: theme.spacing(2),
81
+ }));
82
+
83
+ const StyledEditableHtmlContainer = styled(EditableHtmlContainer)(({ theme }) => ({
84
+ width: '100%',
85
+ marginTop: theme.spacing(2),
86
+ }));
87
+
88
+ const Feedback = ({ value, onChange, type, correct, defaults, toolbarOpts, mathMlOptions = {} }) => {
82
89
  if (!type || type === 'none') {
83
90
  return null;
84
91
  } else if (type === 'default') {
85
92
  return (
86
- <div className={classes.feedbackContainer}>
87
- <ArrowRight className={classes.arrowIcon} />
88
- <TextField
89
- className={classes.text}
93
+ <StyledFeedbackContainer>
94
+ <StyledArrowIcon />
95
+ <StyledTextField
90
96
  label="Feedback Text"
91
97
  value={correct ? defaults.correct : defaults.incorrect}
98
+ variant="standard"
92
99
  />
93
- </div>
100
+ </StyledFeedbackContainer>
94
101
  );
95
102
  } else {
96
103
  return (
97
- <div className={classes.feedbackContainer}>
98
- <ArrowRight className={classes.arrowIcon} />
99
- <EditableHtmlContainer
100
- className={classes.text}
104
+ <StyledFeedbackContainer>
105
+ <StyledArrowIcon />
106
+ <StyledEditableHtmlContainer
101
107
  label="Feedback Text"
102
108
  value={value}
103
109
  onChange={onChange}
104
110
  toolbarOpts={toolbarOpts}
105
111
  mathMlOptions={mathMlOptions}
106
112
  />
107
- </div>
113
+ </StyledFeedbackContainer>
108
114
  );
109
115
  }
110
- });
116
+ };
117
+
118
+ const StyledIndex = styled('span')(({ theme }) => ({
119
+ paddingRight: theme.spacing(1),
120
+ paddingTop: theme.spacing(3),
121
+ }));
122
+
123
+ const StyledTopRow = styled('div')(() => ({
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ }));
127
+
128
+ const StyledToggle = styled('div')(({ theme }) => ({
129
+ flex: '0 1 auto',
130
+ paddingTop: theme.spacing(0.5),
131
+ paddingBottom: 0,
132
+ marginRight: 0,
133
+ marginLeft: theme.spacing(1),
134
+ }));
135
+
136
+ const StyledFeedback = styled('div')(({ theme }) => ({
137
+ flex: '0 1 auto',
138
+ paddingTop: theme.spacing(2),
139
+ paddingLeft: 0,
140
+ marginLeft: 0,
141
+ marginRight: theme.spacing(1),
142
+ }));
143
+
144
+ const StyledFeedbackIcon = styled('div')(() => ({
145
+ margin: 0,
146
+ width: 'inherit',
147
+ }));
148
+
149
+ const StyledDeleteIcon = styled('div')(() => ({
150
+ margin: 0,
151
+ width: 'inherit',
152
+ }));
153
+
154
+ const StyledDelete = styled('div')(({ theme }) => ({
155
+ flex: '0 1 auto',
156
+ paddingTop: theme.spacing(2),
157
+ paddingLeft: 0,
158
+ marginLeft: 0,
159
+ }));
160
+
161
+ const StyledMiddleColumn = styled('div')(({ theme }) => ({
162
+ display: 'flex',
163
+ flex: 1,
164
+ flexDirection: 'column',
165
+ marginRight: theme.spacing(1),
166
+ }));
167
+
168
+ const StyledErrorText = styled('div')(({ theme }) => ({
169
+ fontSize: theme.typography.fontSize - 2,
170
+ color: theme.palette.error.main,
171
+ }));
111
172
 
112
173
  export class ChoiceConfiguration extends React.Component {
113
174
  static propTypes = {
114
- classes: PropTypes.object.isRequired,
115
175
  noLabels: PropTypes.bool,
116
176
  useLetterOrdering: PropTypes.bool,
117
177
  className: PropTypes.string,
@@ -201,7 +261,6 @@ export class ChoiceConfiguration extends React.Component {
201
261
  render() {
202
262
  const {
203
263
  data,
204
- classes,
205
264
  mode,
206
265
  onDelete,
207
266
  defaultFeedback,
@@ -227,45 +286,43 @@ export class ChoiceConfiguration extends React.Component {
227
286
  } = this.props;
228
287
 
229
288
  const InputToggle = mode === 'checkbox' ? InputCheckbox : InputRadio;
230
- const names = classNames(classes.choiceConfiguration, className);
231
289
 
232
290
  return (
233
- <div className={names}>
234
- <div className={classes.topRow}>
291
+ <StyledTopRow>
235
292
  {index > 0 && (
236
- <span className={classes.index} type="title">
293
+ <StyledIndex type="title">
237
294
  {useLetterOrdering ? String.fromCharCode(96 + index).toUpperCase() : index}
238
- </span>
295
+ </StyledIndex>
239
296
  )}
240
297
 
241
- <InputToggle
242
- className={classes.toggle}
243
- onChange={this.onCheckedChange}
244
- label={!noLabels ? 'Correct' : ''}
245
- checked={!!data.correct}
246
- error={noCorrectAnswerError}
247
- />
248
-
249
- <div className={classes.middleColumn}>
250
- <EditableHtmlContainer
251
- className={classes.input}
252
- label={!noLabels ? 'Label' : ''}
253
- value={data.label}
254
- onChange={this.onLabelChange}
255
- imageSupport={imageSupport}
256
- disableImageAlignmentButtons={disableImageAlignmentButtons}
257
- disabled={disabled}
258
- spellCheck={spellCheck}
259
- nonEmpty={nonEmpty}
260
- pluginOpts={pluginOpts}
261
- toolbarOpts={toolbarOpts}
262
- error={error}
263
- uploadSoundSupport={uploadSoundSupport}
264
- mathMlOptions={mathMlOptions}
265
- maxImageWidth={maxImageWidth}
266
- maxImageHeight={maxImageHeight}
298
+ <StyledToggle>
299
+ <InputToggle
300
+ onChange={this.onCheckedChange}
301
+ label={!noLabels ? 'Correct' : ''}
302
+ checked={!!data.correct}
303
+ error={noCorrectAnswerError}
267
304
  />
268
- {error && <div className={classes.errorText}>{error}</div>}
305
+ </StyledToggle>
306
+
307
+ <StyledMiddleColumn>
308
+ <EditableHtmlContainer
309
+ label={!noLabels ? 'Label' : ''}
310
+ value={data.label}
311
+ onChange={this.onLabelChange}
312
+ imageSupport={imageSupport}
313
+ disableImageAlignmentButtons={disableImageAlignmentButtons}
314
+ disabled={disabled}
315
+ spellCheck={spellCheck}
316
+ nonEmpty={nonEmpty}
317
+ pluginOpts={pluginOpts}
318
+ toolbarOpts={toolbarOpts}
319
+ error={error}
320
+ uploadSoundSupport={uploadSoundSupport}
321
+ mathMlOptions={mathMlOptions}
322
+ maxImageWidth={maxImageWidth}
323
+ maxImageHeight={maxImageHeight}
324
+ />
325
+ {error && <StyledErrorText>{error}</StyledErrorText>}
269
326
 
270
327
  {allowFeedBack && (
271
328
  <Feedback
@@ -276,90 +333,38 @@ export class ChoiceConfiguration extends React.Component {
276
333
  toolbarOpts={toolbarOpts}
277
334
  />
278
335
  )}
279
- </div>
336
+ </StyledMiddleColumn>
280
337
 
281
338
  {allowFeedBack && (
282
- <InputContainer className={classes.feedback} label={!noLabels ? 'Feedback' : ''}>
283
- <FeedbackMenu
284
- onChange={this.onFeedbackTypeChange}
285
- value={data.feedback}
286
- classes={{
287
- icon: classes.feedbackIcon,
288
- }}
289
- />
290
- </InputContainer>
339
+ <StyledFeedback>
340
+ <InputContainer label={!noLabels ? 'Feedback' : ''}>
341
+ <StyledFeedbackIcon>
342
+ <FeedbackMenu
343
+ onChange={this.onFeedbackTypeChange}
344
+ value={data.feedback}
345
+ />
346
+ </StyledFeedbackIcon>
347
+ </InputContainer>
348
+ </StyledFeedback>
291
349
  )}
292
350
 
293
351
  {allowDelete && (
294
- <InputContainer className={classes.delete} label={!noLabels ? 'Delete' : ''}>
295
- <IconButton aria-label="delete" className={classes.deleteIcon} onClick={onDelete}>
296
- <ActionDelete />
297
- </IconButton>
298
- </InputContainer>
352
+ <StyledDelete>
353
+ <InputContainer label={!noLabels ? 'Delete' : ''}>
354
+ <StyledDeleteIcon>
355
+ <IconButton
356
+ aria-label="delete"
357
+ onClick={onDelete}
358
+ size="large">
359
+ <ActionDelete />
360
+ </IconButton>
361
+ </StyledDeleteIcon>
362
+ </InputContainer>
363
+ </StyledDelete>
299
364
  )}
300
- </div>
301
- </div>
365
+ </StyledTopRow>
302
366
  );
303
367
  }
304
368
  }
305
369
 
306
- const styles = (theme) => ({
307
- index: {
308
- paddingRight: theme.spacing.unit,
309
- paddingTop: theme.spacing.unit * 3.5,
310
- },
311
- choiceConfiguration: {},
312
- topRow: {
313
- display: 'flex',
314
- },
315
- value: {
316
- flex: '0.5',
317
- paddingRight: theme.spacing.unit,
318
- },
319
- editorHolder: {
320
- marginTop: theme.spacing.unit * 2,
321
- },
322
- toggle: {
323
- flex: '0 1 auto',
324
- paddingTop: theme.spacing.unit / 2,
325
- paddingBottom: 0,
326
- marginRight: 0,
327
- marginLeft: theme.spacing.unit,
328
- },
329
- feedback: {
330
- flex: '0 1 auto',
331
- paddingTop: theme.spacing.unit * 2,
332
- paddingLeft: 0,
333
- marginLeft: 0,
334
- marginRight: theme.spacing.unit,
335
- },
336
- feedbackIcon: {
337
- margin: 0,
338
- width: 'inherit',
339
- },
340
- deleteIcon: {
341
- margin: 0,
342
- width: 'inherit',
343
- },
344
- delete: {
345
- flex: '0 1 auto',
346
- paddingTop: theme.spacing.unit * 2,
347
- paddingLeft: 0,
348
- marginLeft: 0,
349
- },
350
- middleColumn: {
351
- display: 'flex',
352
- flex: 1,
353
- flexDirection: 'column',
354
- marginRight: theme.spacing.unit,
355
- },
356
- input: {
357
- marginRight: 0,
358
- },
359
- errorText: {
360
- fontSize: theme.typography.fontSize - 2,
361
- color: theme.palette.error.main,
362
- },
363
- });
364
-
365
- export default withStyles(styles)(ChoiceConfiguration);
370
+ export default ChoiceConfiguration;
@@ -1,70 +1,140 @@
1
- import React, { PropTypes } from 'react';
2
- import Enzyme, { shallow, mount } from 'enzyme';
3
- import { FeedbackConfig } from '../index';
4
- import FeedbackSelector from '../feedback-selector';
5
- import Adapter from 'enzyme-adapter-react-16';
1
+ import React from 'react';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { FeedbackConfig, buildDefaults } from '../index';
6
5
 
7
- Enzyme.configure({ adapter: new Adapter() });
6
+ // Mock FeedbackSelector to simplify testing
7
+ jest.mock('../feedback-selector', () => {
8
+ return function FeedbackSelector({ label, feedback, onChange }) {
9
+ return (
10
+ <div data-testid="feedback-selector">
11
+ <label>{label}</label>
12
+ <select
13
+ aria-label={label}
14
+ value={feedback.type}
15
+ onChange={(e) => onChange({ ...feedback, type: e.target.value })}
16
+ >
17
+ <option value="default">Default</option>
18
+ <option value="custom">Custom</option>
19
+ <option value="none">None</option>
20
+ </select>
21
+ </div>
22
+ );
23
+ };
24
+ });
8
25
 
9
26
  describe('FeedbackConfig', () => {
10
- describe('render', () => {
11
- let component, selectors;
12
- let feedback = {
13
- correctFeedback: undefined,
14
- correctFeedbackType: 'default',
15
- incorrectFeedback: undefined,
16
- incorrectFeedbackType: 'default',
17
- };
18
-
19
- let defaults = {
20
- correct: 'Correct',
21
- incorrect: 'Incorrect',
22
- partial: 'Nearly',
23
- };
24
-
25
- describe('Feedback Config Component', () => {
26
- it('should exist', () => {
27
- component = shallow(
28
- <FeedbackConfig feedback={feedback} defaults={defaults} onChange={jest.fn()} classes={{}} />,
29
- );
30
-
31
- selectors = component.find(FeedbackSelector);
32
-
33
- expect(selectors.length).toEqual(3);
27
+ const onChange = jest.fn();
28
+
29
+ beforeEach(() => {
30
+ onChange.mockClear();
31
+ });
32
+
33
+ describe('rendering', () => {
34
+ it('renders with default feedback types', () => {
35
+ const feedback = buildDefaults();
36
+
37
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
38
+
39
+ expect(screen.getByText('Feedback')).toBeInTheDocument();
40
+ expect(screen.getByText('If correct, show')).toBeInTheDocument();
41
+ expect(screen.getByText('If partially correct, show')).toBeInTheDocument();
42
+ expect(screen.getByText('If incorrect, show')).toBeInTheDocument();
43
+ });
44
+
45
+ it('renders all three feedback selectors by default', () => {
46
+ const feedback = buildDefaults();
47
+
48
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
49
+
50
+ const selectors = screen.getAllByTestId('feedback-selector');
51
+ expect(selectors).toHaveLength(3);
52
+ });
53
+
54
+ it('does not render partial feedback selector when allowPartial is false', () => {
55
+ const feedback = buildDefaults();
56
+
57
+ render(<FeedbackConfig allowPartial={false} feedback={feedback} onChange={onChange} />);
58
+
59
+ expect(screen.getByText('If correct, show')).toBeInTheDocument();
60
+ expect(screen.queryByText('If partially correct, show')).not.toBeInTheDocument();
61
+ expect(screen.getByText('If incorrect, show')).toBeInTheDocument();
62
+
63
+ const selectors = screen.getAllByTestId('feedback-selector');
64
+ expect(selectors).toHaveLength(2);
65
+ });
66
+ });
67
+
68
+ describe('user interactions', () => {
69
+ it('calls onChange when correct feedback type changes', async () => {
70
+ const user = userEvent.setup();
71
+ const feedback = buildDefaults();
72
+
73
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
74
+
75
+ const correctSelect = screen.getByLabelText('If correct, show');
76
+ await user.selectOptions(correctSelect, 'custom');
77
+
78
+ expect(onChange).toHaveBeenCalledWith(
79
+ expect.objectContaining({
80
+ correct: expect.objectContaining({ type: 'custom' }),
81
+ }),
82
+ );
83
+ });
84
+
85
+ it('calls onChange when incorrect feedback type changes', async () => {
86
+ const user = userEvent.setup();
87
+ const feedback = buildDefaults();
88
+
89
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
90
+
91
+ const incorrectSelect = screen.getByLabelText('If incorrect, show');
92
+ await user.selectOptions(incorrectSelect, 'none');
93
+
94
+ expect(onChange).toHaveBeenCalledWith(
95
+ expect.objectContaining({
96
+ incorrect: expect.objectContaining({ type: 'none' }),
97
+ }),
98
+ );
99
+ });
100
+
101
+ it('calls onChange when partial feedback type changes', async () => {
102
+ const user = userEvent.setup();
103
+ const feedback = buildDefaults();
104
+
105
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
106
+
107
+ const partialSelect = screen.getByLabelText('If partially correct, show');
108
+ await user.selectOptions(partialSelect, 'custom');
109
+
110
+ expect(onChange).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ partial: expect.objectContaining({ type: 'custom' }),
113
+ }),
114
+ );
115
+ });
116
+ });
117
+
118
+ describe('buildDefaults helper', () => {
119
+ it('returns default feedback configuration', () => {
120
+ const defaults = buildDefaults();
121
+
122
+ expect(defaults).toEqual({
123
+ correct: { type: 'default', default: 'Correct' },
124
+ incorrect: { type: 'default', default: 'Incorrect' },
125
+ partial: { type: 'default', default: 'Nearly' },
34
126
  });
127
+ });
35
128
 
36
- describe('props', () => {
37
- it('should not render optionally correct if optional is not needed', () => {
38
- component = shallow(
39
- <FeedbackConfig
40
- allowPartial={false}
41
- feedback={feedback}
42
- defaults={defaults}
43
- onChange={jest.fn()}
44
- classes={{}}
45
- />,
46
- );
47
-
48
- selectors = component.find(FeedbackSelector);
49
-
50
- expect(selectors.length).toEqual(2);
51
- });
129
+ it('merges custom values with defaults', () => {
130
+ const defaults = buildDefaults({
131
+ correct: { type: 'custom', custom: 'Great job!' },
52
132
  });
53
133
 
54
- describe('snapshot', () => {
55
- it('matches the snapshot', () => {
56
- component = shallow(
57
- <FeedbackConfig
58
- allowPartial={false}
59
- feedback={feedback}
60
- defaults={defaults}
61
- onChange={jest.fn()}
62
- classes={{}}
63
- />,
64
- );
65
-
66
- expect(component).toMatchSnapshot();
67
- });
134
+ expect(defaults).toEqual({
135
+ correct: { type: 'custom', default: 'Correct', custom: 'Great job!' },
136
+ incorrect: { type: 'default', default: 'Incorrect' },
137
+ partial: { type: 'default', default: 'Nearly' },
68
138
  });
69
139
  });
70
140
  });