@pie-lib/config-ui 12.0.0-beta.5 → 12.0.0-next.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 (116) hide show
  1. package/CHANGELOG.json +8 -1653
  2. package/CHANGELOG.md +345 -4
  3. package/LICENSE.md +5 -0
  4. package/NEXT.CHANGELOG.json +1 -0
  5. package/lib/alert-dialog.js +40 -10
  6. package/lib/alert-dialog.js.map +1 -1
  7. package/lib/checkbox.js +58 -48
  8. package/lib/checkbox.js.map +1 -1
  9. package/lib/choice-configuration/feedback-menu.js +24 -26
  10. package/lib/choice-configuration/feedback-menu.js.map +1 -1
  11. package/lib/choice-configuration/index.js +182 -185
  12. package/lib/choice-configuration/index.js.map +1 -1
  13. package/lib/choice-utils.js +5 -7
  14. package/lib/choice-utils.js.map +1 -1
  15. package/lib/feedback-config/feedback-selector.js +69 -73
  16. package/lib/feedback-config/feedback-selector.js.map +1 -1
  17. package/lib/feedback-config/group.js +22 -25
  18. package/lib/feedback-config/group.js.map +1 -1
  19. package/lib/feedback-config/index.js +41 -44
  20. package/lib/feedback-config/index.js.map +1 -1
  21. package/lib/form-section.js +31 -25
  22. package/lib/form-section.js.map +1 -1
  23. package/lib/help.js +37 -47
  24. package/lib/help.js.map +1 -1
  25. package/lib/index.js +1 -2
  26. package/lib/index.js.map +1 -1
  27. package/lib/input.js +12 -17
  28. package/lib/input.js.map +1 -1
  29. package/lib/inputs.js +58 -67
  30. package/lib/inputs.js.map +1 -1
  31. package/lib/langs.js +56 -70
  32. package/lib/langs.js.map +1 -1
  33. package/lib/layout/config-layout.js +78 -47
  34. package/lib/layout/config-layout.js.map +1 -1
  35. package/lib/layout/index.js.map +1 -1
  36. package/lib/layout/layout-contents.js +58 -60
  37. package/lib/layout/layout-contents.js.map +1 -1
  38. package/lib/layout/settings-box.js +25 -33
  39. package/lib/layout/settings-box.js.map +1 -1
  40. package/lib/mui-box/index.js +41 -50
  41. package/lib/mui-box/index.js.map +1 -1
  42. package/lib/number-text-field-custom.js +151 -89
  43. package/lib/number-text-field-custom.js.map +1 -1
  44. package/lib/number-text-field.js +74 -63
  45. package/lib/number-text-field.js.map +1 -1
  46. package/lib/radio-with-label.js +30 -16
  47. package/lib/radio-with-label.js.map +1 -1
  48. package/lib/settings/display-size.js +16 -20
  49. package/lib/settings/display-size.js.map +1 -1
  50. package/lib/settings/index.js +13 -19
  51. package/lib/settings/index.js.map +1 -1
  52. package/lib/settings/panel.js +140 -141
  53. package/lib/settings/panel.js.map +1 -1
  54. package/lib/settings/settings-radio-label.js +29 -16
  55. package/lib/settings/settings-radio-label.js.map +1 -1
  56. package/lib/settings/toggle.js +39 -25
  57. package/lib/settings/toggle.js.map +1 -1
  58. package/lib/tabs/index.js +18 -30
  59. package/lib/tabs/index.js.map +1 -1
  60. package/lib/tags-input/index.js +49 -61
  61. package/lib/tags-input/index.js.map +1 -1
  62. package/lib/two-choice.js +33 -43
  63. package/lib/two-choice.js.map +1 -1
  64. package/lib/with-stateful-model.js +8 -12
  65. package/lib/with-stateful-model.js.map +1 -1
  66. package/package.json +22 -11
  67. package/src/__tests__/alert-dialog.test.jsx +283 -0
  68. package/src/__tests__/checkbox.test.jsx +249 -0
  69. package/src/__tests__/choice-utils.test.js +12 -0
  70. package/src/__tests__/form-section.test.jsx +334 -0
  71. package/src/__tests__/help.test.jsx +184 -0
  72. package/src/__tests__/input.test.jsx +192 -0
  73. package/src/__tests__/langs.test.jsx +457 -0
  74. package/src/__tests__/number-text-field-custom.test.jsx +438 -0
  75. package/src/__tests__/number-text-field.test.jsx +341 -0
  76. package/src/__tests__/radio-with-label.test.jsx +259 -0
  77. package/src/__tests__/settings-panel.test.js +187 -0
  78. package/src/__tests__/settings.test.jsx +515 -0
  79. package/src/__tests__/tabs.test.jsx +193 -0
  80. package/src/__tests__/two-choice.test.js +110 -0
  81. package/src/__tests__/with-stateful-model.test.jsx +145 -0
  82. package/src/alert-dialog.jsx +30 -8
  83. package/src/checkbox.jsx +43 -37
  84. package/src/choice-configuration/__tests__/feedback-menu.test.jsx +163 -0
  85. package/src/choice-configuration/__tests__/index.test.jsx +234 -0
  86. package/src/choice-configuration/feedback-menu.jsx +6 -6
  87. package/src/choice-configuration/index.jsx +208 -192
  88. package/src/feedback-config/__tests__/feedback-config.test.jsx +141 -0
  89. package/src/feedback-config/__tests__/feedback-selector.test.jsx +107 -0
  90. package/src/feedback-config/feedback-selector.jsx +52 -53
  91. package/src/feedback-config/group.jsx +21 -22
  92. package/src/feedback-config/index.jsx +27 -29
  93. package/src/form-section.jsx +26 -18
  94. package/src/help.jsx +20 -28
  95. package/src/input.jsx +1 -1
  96. package/src/inputs.jsx +35 -44
  97. package/src/langs.jsx +41 -46
  98. package/src/layout/__tests__/config.layout.test.jsx +59 -0
  99. package/src/layout/__tests__/layout-content.test.jsx +3 -0
  100. package/src/layout/config-layout.jsx +53 -23
  101. package/src/layout/layout-contents.jsx +38 -40
  102. package/src/layout/settings-box.jsx +16 -19
  103. package/src/mui-box/index.jsx +35 -43
  104. package/src/number-text-field-custom.jsx +117 -65
  105. package/src/number-text-field.jsx +51 -34
  106. package/src/radio-with-label.jsx +26 -10
  107. package/src/settings/display-size.jsx +12 -11
  108. package/src/settings/index.js +2 -1
  109. package/src/settings/panel.jsx +101 -92
  110. package/src/settings/settings-radio-label.jsx +26 -10
  111. package/src/settings/toggle.jsx +37 -18
  112. package/src/tabs/index.jsx +8 -8
  113. package/src/tags-input/__tests__/index.test.jsx +113 -0
  114. package/src/tags-input/index.jsx +35 -38
  115. package/src/two-choice.jsx +15 -19
  116. package/README.md +0 -12
@@ -1,115 +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 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';
6
8
  import { InputContainer } from '@pie-lib/render-ui';
7
- import EditableHtml from '@pie-lib/editable-html';
9
+ // import EditableHtml from '@pie-lib/editable-html';
8
10
  import { InputCheckbox, InputRadio } from '../inputs';
9
11
  import FeedbackMenu from './feedback-menu';
10
- import ActionDelete from '@material-ui/icons/Delete';
11
- import ArrowRight from '@material-ui/icons/SubdirectoryArrowRight';
12
- import IconButton from '@material-ui/core/IconButton';
13
-
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
- toolbarOpts,
32
- error,
33
- maxImageWidth,
34
- maxImageHeight,
35
- uploadSoundSupport,
36
- mathMlOptions = {},
37
- }) => {
38
- const names = classNames(classes.labelContainer, className);
39
12
 
40
- return (
41
- <InputContainer label={label} className={names}>
42
- <div className={classes.editorHolder}>
43
- <EditableHtml
44
- markup={value || ''}
45
- disabled={disabled}
46
- spellCheck={spellCheck}
47
- nonEmpty={nonEmpty}
48
- onChange={onChange}
49
- imageSupport={imageSupport}
50
- disableImageAlignmentButtons={disableImageAlignmentButtons}
51
- className={classes.editor}
52
- toolbarOpts={toolbarOpts}
53
- error={error}
54
- maxImageWidth={maxImageWidth}
55
- maxImageHeight={maxImageHeight}
56
- uploadSoundSupport={uploadSoundSupport}
57
- languageCharactersProps={[{ language: 'spanish' }, { language: 'special' }]}
58
- mathMlOptions={mathMlOptions}
59
- />
60
- </div>
61
- </InputContainer>
62
- );
63
- },
64
- );
65
-
66
- const Feedback = withStyles((theme) => ({
67
- text: {
68
- width: '100%',
69
- },
70
- feedbackContainer: {
71
- position: 'relative',
72
- },
73
- arrowIcon: {
74
- fill: theme.palette.grey[400],
75
- left: -56,
76
- position: 'absolute',
77
- top: 20,
78
- },
79
- }))(({ value, onChange, type, correct, classes, defaults, toolbarOpts, mathMlOptions = {} }) => {
13
+ // - mathquill error window not defined
14
+ let EditableHtml;
15
+ if (typeof window !== 'undefined') {
16
+ EditableHtml = require('@pie-lib/editable-html')['default'];
17
+ }
18
+
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 = {} }) => {
80
89
  if (!type || type === 'none') {
81
90
  return null;
82
91
  } else if (type === 'default') {
83
92
  return (
84
- <div className={classes.feedbackContainer}>
85
- <ArrowRight className={classes.arrowIcon} />
86
- <TextField
87
- className={classes.text}
93
+ <StyledFeedbackContainer>
94
+ <StyledArrowIcon />
95
+ <StyledTextField
88
96
  label="Feedback Text"
89
97
  value={correct ? defaults.correct : defaults.incorrect}
98
+ variant="standard"
90
99
  />
91
- </div>
100
+ </StyledFeedbackContainer>
92
101
  );
93
102
  } else {
94
103
  return (
95
- <div className={classes.feedbackContainer}>
96
- <ArrowRight className={classes.arrowIcon} />
97
- <EditableHtmlContainer
98
- className={classes.text}
104
+ <StyledFeedbackContainer>
105
+ <StyledArrowIcon />
106
+ <StyledEditableHtmlContainer
99
107
  label="Feedback Text"
100
108
  value={value}
101
109
  onChange={onChange}
102
110
  toolbarOpts={toolbarOpts}
103
111
  mathMlOptions={mathMlOptions}
104
112
  />
105
- </div>
113
+ </StyledFeedbackContainer>
106
114
  );
107
115
  }
108
- });
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
+ }));
109
172
 
110
173
  export class ChoiceConfiguration extends React.Component {
111
174
  static propTypes = {
112
- classes: PropTypes.object.isRequired,
113
175
  noLabels: PropTypes.bool,
114
176
  useLetterOrdering: PropTypes.bool,
115
177
  className: PropTypes.string,
@@ -139,8 +201,11 @@ export class ChoiceConfiguration extends React.Component {
139
201
  allowDelete: PropTypes.bool,
140
202
  noCorrectAnswerError: PropTypes.string,
141
203
  spellCheck: PropTypes.bool,
204
+ pluginOpts: PropTypes.object,
142
205
  toolbarOpts: PropTypes.object,
143
206
  uploadSoundSupport: PropTypes.object,
207
+ maxImageWidth: PropTypes.number,
208
+ maxImageHeight: PropTypes.number,
144
209
  };
145
210
 
146
211
  static defaultProps = {
@@ -196,7 +261,6 @@ export class ChoiceConfiguration extends React.Component {
196
261
  render() {
197
262
  const {
198
263
  data,
199
- classes,
200
264
  mode,
201
265
  onDelete,
202
266
  defaultFeedback,
@@ -211,50 +275,54 @@ export class ChoiceConfiguration extends React.Component {
211
275
  nonEmpty,
212
276
  allowFeedBack,
213
277
  allowDelete,
278
+ pluginOpts,
214
279
  toolbarOpts,
215
280
  error,
216
281
  noCorrectAnswerError,
217
282
  uploadSoundSupport,
283
+ maxImageWidth,
284
+ maxImageHeight,
218
285
  mathMlOptions = {},
219
286
  } = this.props;
220
287
 
221
288
  const InputToggle = mode === 'checkbox' ? InputCheckbox : InputRadio;
222
- const names = classNames(classes.choiceConfiguration, className);
223
289
 
224
290
  return (
225
- <div className={names}>
226
- <div className={classes.topRow}>
291
+ <StyledTopRow>
227
292
  {index > 0 && (
228
- <span className={classes.index} type="title">
293
+ <StyledIndex type="title">
229
294
  {useLetterOrdering ? String.fromCharCode(96 + index).toUpperCase() : index}
230
- </span>
295
+ </StyledIndex>
231
296
  )}
232
297
 
233
- <InputToggle
234
- className={classes.toggle}
235
- onChange={this.onCheckedChange}
236
- label={!noLabels ? 'Correct' : ''}
237
- checked={!!data.correct}
238
- error={noCorrectAnswerError}
239
- />
240
-
241
- <div className={classes.middleColumn}>
242
- <EditableHtmlContainer
243
- className={classes.input}
244
- label={!noLabels ? 'Label' : ''}
245
- value={data.label}
246
- onChange={this.onLabelChange}
247
- imageSupport={imageSupport}
248
- disableImageAlignmentButtons={disableImageAlignmentButtons}
249
- disabled={disabled}
250
- spellCheck={spellCheck}
251
- nonEmpty={nonEmpty}
252
- toolbarOpts={toolbarOpts}
253
- error={error}
254
- uploadSoundSupport={uploadSoundSupport}
255
- mathMlOptions={mathMlOptions}
298
+ <StyledToggle>
299
+ <InputToggle
300
+ onChange={this.onCheckedChange}
301
+ label={!noLabels ? 'Correct' : ''}
302
+ checked={!!data.correct}
303
+ error={noCorrectAnswerError}
256
304
  />
257
- {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>}
258
326
 
259
327
  {allowFeedBack && (
260
328
  <Feedback
@@ -265,90 +333,38 @@ export class ChoiceConfiguration extends React.Component {
265
333
  toolbarOpts={toolbarOpts}
266
334
  />
267
335
  )}
268
- </div>
336
+ </StyledMiddleColumn>
269
337
 
270
338
  {allowFeedBack && (
271
- <InputContainer className={classes.feedback} label={!noLabels ? 'Feedback' : ''}>
272
- <FeedbackMenu
273
- onChange={this.onFeedbackTypeChange}
274
- value={data.feedback}
275
- classes={{
276
- icon: classes.feedbackIcon,
277
- }}
278
- />
279
- </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>
280
349
  )}
281
350
 
282
351
  {allowDelete && (
283
- <InputContainer className={classes.delete} label={!noLabels ? 'Delete' : ''}>
284
- <IconButton aria-label="delete" className={classes.deleteIcon} onClick={onDelete}>
285
- <ActionDelete />
286
- </IconButton>
287
- </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>
288
364
  )}
289
- </div>
290
- </div>
365
+ </StyledTopRow>
291
366
  );
292
367
  }
293
368
  }
294
369
 
295
- const styles = (theme) => ({
296
- index: {
297
- paddingRight: theme.spacing.unit,
298
- paddingTop: theme.spacing.unit * 3.5,
299
- },
300
- choiceConfiguration: {},
301
- topRow: {
302
- display: 'flex',
303
- },
304
- value: {
305
- flex: '0.5',
306
- paddingRight: theme.spacing.unit,
307
- },
308
- editorHolder: {
309
- marginTop: theme.spacing.unit * 2,
310
- },
311
- toggle: {
312
- flex: '0 1 auto',
313
- paddingTop: theme.spacing.unit / 2,
314
- paddingBottom: 0,
315
- marginRight: 0,
316
- marginLeft: theme.spacing.unit,
317
- },
318
- feedback: {
319
- flex: '0 1 auto',
320
- paddingTop: theme.spacing.unit * 2,
321
- paddingLeft: 0,
322
- marginLeft: 0,
323
- marginRight: theme.spacing.unit,
324
- },
325
- feedbackIcon: {
326
- margin: 0,
327
- width: 'inherit',
328
- },
329
- deleteIcon: {
330
- margin: 0,
331
- width: 'inherit',
332
- },
333
- delete: {
334
- flex: '0 1 auto',
335
- paddingTop: theme.spacing.unit * 2,
336
- paddingLeft: 0,
337
- marginLeft: 0,
338
- },
339
- middleColumn: {
340
- display: 'flex',
341
- flex: 1,
342
- flexDirection: 'column',
343
- marginRight: theme.spacing.unit,
344
- },
345
- input: {
346
- marginRight: 0,
347
- },
348
- errorText: {
349
- fontSize: theme.typography.fontSize - 2,
350
- color: theme.palette.error.main,
351
- },
352
- });
353
-
354
- export default withStyles(styles)(ChoiceConfiguration);
370
+ export default ChoiceConfiguration;
@@ -0,0 +1,141 @@
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';
5
+
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
+ });
25
+
26
+ describe('FeedbackConfig', () => {
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' },
126
+ });
127
+ });
128
+
129
+ it('merges custom values with defaults', () => {
130
+ const defaults = buildDefaults({
131
+ correct: { type: 'custom', custom: 'Great job!' },
132
+ });
133
+
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' },
138
+ });
139
+ });
140
+ });
141
+ });