@pie-lib/render-ui 5.1.1-next.0 → 5.2.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 (48) hide show
  1. package/lib/assets/enableAudioAutoplayImage.js +0 -1
  2. package/lib/assets/enableAudioAutoplayImage.js.map +1 -1
  3. package/lib/collapsible/index.js +0 -3
  4. package/lib/collapsible/index.js.map +1 -1
  5. package/lib/color.js +0 -4
  6. package/lib/color.js.map +1 -1
  7. package/lib/feedback.js +0 -9
  8. package/lib/feedback.js.map +1 -1
  9. package/lib/has-media.js +0 -1
  10. package/lib/has-media.js.map +1 -1
  11. package/lib/has-text.js +0 -2
  12. package/lib/has-text.js.map +1 -1
  13. package/lib/html-and-math.js +0 -2
  14. package/lib/html-and-math.js.map +1 -1
  15. package/lib/index.js +0 -7
  16. package/lib/index.js.map +1 -1
  17. package/lib/input-container.js +0 -1
  18. package/lib/input-container.js.map +1 -1
  19. package/lib/preview-layout.js +0 -2
  20. package/lib/preview-layout.js.map +1 -1
  21. package/lib/preview-prompt.js +0 -26
  22. package/lib/preview-prompt.js.map +1 -1
  23. package/lib/purpose.js +0 -3
  24. package/lib/purpose.js.map +1 -1
  25. package/lib/readable.js +0 -3
  26. package/lib/readable.js.map +1 -1
  27. package/lib/response-indicators.js +0 -8
  28. package/lib/response-indicators.js.map +1 -1
  29. package/lib/ui-layout.js +0 -3
  30. package/lib/ui-layout.js.map +1 -1
  31. package/lib/withUndoReset.js +2 -14
  32. package/lib/withUndoReset.js.map +1 -1
  33. package/package.json +11 -7
  34. package/src/__tests__/color.test.js +248 -1
  35. package/src/__tests__/feedback.test.jsx +279 -0
  36. package/src/__tests__/has-media.test.js +0 -1
  37. package/src/__tests__/has-text.test.js +0 -1
  38. package/src/__tests__/input-container.test.jsx +328 -0
  39. package/src/__tests__/preview-layout.test.jsx +349 -0
  40. package/src/__tests__/preview-prompt.test.jsx +320 -0
  41. package/src/__tests__/response-indicators.test.jsx +1 -1
  42. package/src/__tests__/ui-layout.test.jsx +3 -1
  43. package/src/color.js +9 -7
  44. package/src/feedback.jsx +4 -14
  45. package/src/input-container.jsx +2 -5
  46. package/src/preview-layout.jsx +6 -1
  47. package/src/ui-layout.jsx +1 -2
  48. package/NEXT.CHANGELOG.json +0 -1
@@ -1,7 +1,14 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
3
4
  import PreviewPrompt from '../preview-prompt';
4
5
 
6
+ jest.mock('@pie-lib/math-rendering', () => ({
7
+ renderMath: jest.fn(),
8
+ }));
9
+
10
+ const { renderMath } = require('@pie-lib/math-rendering');
11
+
5
12
  describe('Prompt without Custom tag', () => {
6
13
  const defaultProps = {
7
14
  classes: {},
@@ -57,3 +64,316 @@ describe('Prompt with Custom tag', () => {
57
64
  });
58
65
  });
59
66
  });
67
+
68
+ describe('PreviewPrompt - Extended Tests', () => {
69
+ beforeEach(() => {
70
+ jest.clearAllMocks();
71
+ document.body.innerHTML = '';
72
+ });
73
+
74
+ describe('tagName variations', () => {
75
+ it('should render as legend when tagName is "legend"', () => {
76
+ const { container } = render(<PreviewPrompt prompt="Legend text" tagName="legend" />);
77
+ expect(container.firstChild.tagName.toLowerCase()).toBe('legend');
78
+ });
79
+
80
+ it('should render as div by default when no tagName', () => {
81
+ const { container } = render(<PreviewPrompt prompt="Default text" />);
82
+ expect(container.firstChild.tagName.toLowerCase()).toBe('div');
83
+ });
84
+
85
+ it('should apply legend class when tagName is "legend"', () => {
86
+ const { container } = render(<PreviewPrompt prompt="Legend text" tagName="legend" />);
87
+ expect(container.firstChild).toHaveClass('legend');
88
+ });
89
+
90
+ it('should render as p tag when specified', () => {
91
+ const { container } = render(<PreviewPrompt prompt="Paragraph text" tagName="p" />);
92
+ expect(container.firstChild.tagName.toLowerCase()).toBe('p');
93
+ });
94
+ });
95
+
96
+ describe('className handling', () => {
97
+ it('should apply single className', () => {
98
+ const { container } = render(<PreviewPrompt prompt="Test" className="custom-class" />);
99
+ expect(container.firstChild).toHaveClass('custom-class');
100
+ });
101
+
102
+ it('should apply defaultClassName', () => {
103
+ const { container } = render(<PreviewPrompt prompt="Test" defaultClassName="default-class" />);
104
+ expect(container.firstChild).toHaveClass('default-class');
105
+ });
106
+
107
+ it('should combine className and defaultClassName', () => {
108
+ const { container } = render(<PreviewPrompt prompt="Test" className="custom" defaultClassName="default" />);
109
+ expect(container.firstChild).toHaveClass('custom');
110
+ expect(container.firstChild).toHaveClass('default');
111
+ });
112
+
113
+ it('should add legend class when tagName is legend', () => {
114
+ const { container } = render(<PreviewPrompt prompt="Test" tagName="legend" className="custom" />);
115
+ expect(container.firstChild).toHaveClass('custom');
116
+ expect(container.firstChild).toHaveClass('legend');
117
+ });
118
+ });
119
+
120
+ describe('onClick handler', () => {
121
+ it('should call onClick when clicked', async () => {
122
+ const onClick = jest.fn();
123
+ const user = userEvent.setup();
124
+
125
+ render(<PreviewPrompt prompt="Clickable text" onClick={onClick} />);
126
+
127
+ await user.click(screen.getByText('Clickable text'));
128
+ expect(onClick).toHaveBeenCalledTimes(1);
129
+ });
130
+
131
+ it('should work without onClick handler', async () => {
132
+ const user = userEvent.setup();
133
+
134
+ render(<PreviewPrompt prompt="Text without handler" />);
135
+
136
+ await user.click(screen.getByText('Text without handler'));
137
+ });
138
+
139
+ it('should call onClick multiple times', async () => {
140
+ const onClick = jest.fn();
141
+ const user = userEvent.setup();
142
+
143
+ render(<PreviewPrompt prompt="Multi-click text" onClick={onClick} />);
144
+
145
+ await user.click(screen.getByText('Multi-click text'));
146
+ await user.click(screen.getByText('Multi-click text'));
147
+ await user.click(screen.getByText('Multi-click text'));
148
+
149
+ expect(onClick).toHaveBeenCalledTimes(3);
150
+ });
151
+ });
152
+
153
+ describe('newline replacement', () => {
154
+ it('should replace \\embed{newLine}[] with \\newline', () => {
155
+ const promptWithNewline = 'Line 1\\embed{newLine}[]Line 2';
156
+ const { container } = render(<PreviewPrompt prompt={promptWithNewline} />);
157
+
158
+ expect(container.innerHTML).toContain('\\newline');
159
+ expect(container.innerHTML).not.toContain('\\embed{newLine}[]');
160
+ });
161
+
162
+ it('should replace multiple newlines', () => {
163
+ const promptWithNewlines = 'A\\embed{newLine}[]B\\embed{newLine}[]C';
164
+ const { container } = render(<PreviewPrompt prompt={promptWithNewlines} />);
165
+
166
+ const newlineCount = (container.innerHTML.match(/\\newline/g) || []).length;
167
+ expect(newlineCount).toBe(2);
168
+ });
169
+
170
+ it('should handle prompt without newlines', () => {
171
+ const normalPrompt = 'Normal text without newlines';
172
+ render(<PreviewPrompt prompt={normalPrompt} />);
173
+
174
+ expect(screen.getByText('Normal text without newlines')).toBeInTheDocument();
175
+ });
176
+ });
177
+
178
+ describe('audio handling', () => {
179
+ it('should parse audio tag and add source element', () => {
180
+ const promptWithAudio = '<audio src="test.mp3"></audio>';
181
+ render(<PreviewPrompt prompt={promptWithAudio} />);
182
+
183
+ const audio = document.querySelector('audio');
184
+ expect(audio).toBeInTheDocument();
185
+ expect(audio).toHaveAttribute('id', 'pie-prompt-audio-player');
186
+
187
+ const source = audio.querySelector('source');
188
+ expect(source).toBeInTheDocument();
189
+ expect(source).toHaveAttribute('src', 'test.mp3');
190
+ expect(source).toHaveAttribute('type', 'audio/mp3');
191
+ });
192
+
193
+ it('should remove src attribute from audio tag after adding source', () => {
194
+ const promptWithAudio = '<audio src="test.mp3"></audio>';
195
+ render(<PreviewPrompt prompt={promptWithAudio} />);
196
+
197
+ const audio = document.querySelector('audio');
198
+ expect(audio).not.toHaveAttribute('src');
199
+ });
200
+
201
+ it('should handle custom audio button', () => {
202
+ const customAudioButton = {
203
+ playImage: 'play.png',
204
+ pauseImage: 'pause.png',
205
+ };
206
+ const promptWithAudio = '<audio src="test.mp3"></audio>';
207
+
208
+ render(<PreviewPrompt prompt={promptWithAudio} customAudioButton={customAudioButton} />);
209
+
210
+ const audio = document.getElementById('pie-prompt-audio-player');
211
+ expect(audio).toHaveStyle({ display: 'none' });
212
+
213
+ const playButton = document.getElementById('play-audio-button');
214
+ expect(playButton).toBeInTheDocument();
215
+ });
216
+
217
+ it('should not add custom button when customAudioButton is not provided', () => {
218
+ const promptWithAudio = '<audio src="test.mp3"></audio>';
219
+ render(<PreviewPrompt prompt={promptWithAudio} />);
220
+
221
+ const playButton = document.getElementById('play-audio-button');
222
+ expect(playButton).not.toBeInTheDocument();
223
+ });
224
+ });
225
+
226
+ describe('image alignment', () => {
227
+ it('should align images with alignment attribute', () => {
228
+ const promptWithImage = '<img src="test.jpg" alignment="center" />';
229
+ render(<PreviewPrompt prompt={promptWithImage} />);
230
+
231
+ const images = document.querySelectorAll('img');
232
+ expect(images.length).toBeGreaterThan(0);
233
+ });
234
+
235
+ it('should handle images without alignment attribute', () => {
236
+ const promptWithImage = '<img src="test.jpg" />';
237
+ render(<PreviewPrompt prompt={promptWithImage} />);
238
+
239
+ const img = document.querySelector('img');
240
+ expect(img).toBeInTheDocument();
241
+ });
242
+ });
243
+
244
+ describe('edge cases', () => {
245
+ it('should handle empty prompt', () => {
246
+ const { container } = render(<PreviewPrompt prompt="" />);
247
+ expect(container.firstChild).toBeInTheDocument();
248
+ });
249
+
250
+ it('should handle undefined prompt', () => {
251
+ const { container } = render(<PreviewPrompt prompt={undefined} />);
252
+ expect(container.firstChild).toBeInTheDocument();
253
+ });
254
+
255
+ it('should handle null prompt', () => {
256
+ const { container } = render(<PreviewPrompt prompt={null} />);
257
+ expect(container.firstChild).toBeInTheDocument();
258
+ });
259
+
260
+ it('should handle very long prompt', () => {
261
+ const longPrompt = 'A'.repeat(10000);
262
+ render(<PreviewPrompt prompt={longPrompt} />);
263
+ expect(screen.getByText(longPrompt)).toBeInTheDocument();
264
+ });
265
+
266
+ it('should handle prompt with special HTML characters', () => {
267
+ const specialPrompt = '<div>Test & "quotes" \' apostrophe</div>';
268
+ render(<PreviewPrompt prompt={specialPrompt} />);
269
+ expect(screen.getByText(/Test/)).toBeInTheDocument();
270
+ });
271
+
272
+ it('should handle prompt with script tags safely', () => {
273
+ const scriptPrompt = '<script>alert("XSS")</script>Safe text';
274
+ const { container } = render(<PreviewPrompt prompt={scriptPrompt} />);
275
+ expect(container).toBeInTheDocument();
276
+ });
277
+ });
278
+
279
+ describe('dangerouslySetInnerHTML', () => {
280
+ it('should render HTML using dangerouslySetInnerHTML', () => {
281
+ const htmlPrompt = '<strong>Bold</strong> and <em>italic</em>';
282
+ render(<PreviewPrompt prompt={htmlPrompt} />);
283
+
284
+ expect(screen.getByText('Bold')).toBeInTheDocument();
285
+ expect(screen.getByText(/italic/)).toBeInTheDocument();
286
+ });
287
+
288
+ it('should render complex nested HTML', () => {
289
+ const complexPrompt = `
290
+ <div>
291
+ <h3>Title</h3>
292
+ <p>Paragraph 1</p>
293
+ <ul>
294
+ <li>Item 1</li>
295
+ <li>Item 2</li>
296
+ </ul>
297
+ </div>
298
+ `;
299
+ render(<PreviewPrompt prompt={complexPrompt} />);
300
+
301
+ expect(screen.getByText('Title')).toBeInTheDocument();
302
+ expect(screen.getByText('Paragraph 1')).toBeInTheDocument();
303
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
304
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
305
+ });
306
+
307
+ it('should render tables', () => {
308
+ const tablePrompt = `
309
+ <table>
310
+ <tbody>
311
+ <tr><th>Header</th><td>Data</td></tr>
312
+ </tbody>
313
+ </table>
314
+ `;
315
+ render(<PreviewPrompt prompt={tablePrompt} />);
316
+
317
+ expect(screen.getByText('Header')).toBeInTheDocument();
318
+ expect(screen.getByText('Data')).toBeInTheDocument();
319
+ });
320
+ });
321
+
322
+ describe('component lifecycle', () => {
323
+ it('should cleanup on unmount', () => {
324
+ const { unmount } = render(<PreviewPrompt prompt="Test" />);
325
+ unmount();
326
+ // Should not throw errors on unmount
327
+ });
328
+
329
+ it('should update when props change', () => {
330
+ const { rerender } = render(<PreviewPrompt prompt="Initial" />);
331
+ rerender(<PreviewPrompt prompt="Updated" />);
332
+
333
+ expect(screen.getByText('Updated')).toBeInTheDocument();
334
+ expect(screen.queryByText('Initial')).not.toBeInTheDocument();
335
+ });
336
+
337
+ it('should handle rapid prop changes', () => {
338
+ const { rerender } = render(<PreviewPrompt prompt="V1" />);
339
+ rerender(<PreviewPrompt prompt="V2" />);
340
+ rerender(<PreviewPrompt prompt="V3" />);
341
+ rerender(<PreviewPrompt prompt="V4" />);
342
+
343
+ expect(screen.getByText('V4')).toBeInTheDocument();
344
+ });
345
+ });
346
+
347
+ describe('accessibility', () => {
348
+ it('should have preview-prompt id for accessibility', () => {
349
+ render(<PreviewPrompt prompt="Accessible text" />);
350
+ const element = document.getElementById('preview-prompt');
351
+ expect(element).toBeInTheDocument();
352
+ });
353
+
354
+ it('should preserve semantic HTML', () => {
355
+ const semanticPrompt = '<strong>Important:</strong> Read carefully';
356
+ render(<PreviewPrompt prompt={semanticPrompt} />);
357
+
358
+ const strong = document.querySelector('strong');
359
+ expect(strong).toBeInTheDocument();
360
+ expect(strong).toHaveTextContent('Important:');
361
+ });
362
+ });
363
+
364
+ describe('multiple instances', () => {
365
+ it('should handle multiple PreviewPrompt components', () => {
366
+ const { container } = render(
367
+ <div>
368
+ <PreviewPrompt prompt="First prompt" />
369
+ <PreviewPrompt prompt="Second prompt" />
370
+ <PreviewPrompt prompt="Third prompt" />
371
+ </div>,
372
+ );
373
+
374
+ expect(screen.getByText('First prompt')).toBeInTheDocument();
375
+ expect(screen.getByText('Second prompt')).toBeInTheDocument();
376
+ expect(screen.getByText('Third prompt')).toBeInTheDocument();
377
+ });
378
+ });
379
+ });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
- import { Correct, Incorrect, PartiallyCorrect, NothingSubmitted } from '../response-indicators';
4
+ import { Correct, Incorrect, NothingSubmitted, PartiallyCorrect } from '../response-indicators';
5
5
 
6
6
  // Mock the icons
7
7
  jest.mock('@pie-lib/icons', () => ({
@@ -23,7 +23,9 @@ describe('UiLayout', () => {
23
23
  });
24
24
 
25
25
  it('applies the correct classes', () => {
26
- const { container } = render(<UiLayout className="custom-class" classes={mockClasses} fontSizeFactor={fontSizeFactor} />);
26
+ const { container } = render(
27
+ <UiLayout className="custom-class" classes={mockClasses} fontSizeFactor={fontSizeFactor} />,
28
+ );
27
29
  const div = container.querySelector('.custom-class');
28
30
  expect(div).toBeInTheDocument();
29
31
  });
package/src/color.js CHANGED
@@ -1,4 +1,4 @@
1
- import { green, orange, pink, indigo, red } from '@mui/material/colors';
1
+ import { green, indigo, orange, pink, red } from '@mui/material/colors';
2
2
 
3
3
  export const defaults = {
4
4
  TEXT: 'black',
@@ -49,12 +49,14 @@ export const defaults = {
49
49
 
50
50
  Object.freeze(defaults);
51
51
 
52
- export const v = (prefix) => (...args) => {
53
- const fallback = args.pop();
54
- return args.reduceRight((acc, v) => {
55
- return `var(--${prefix}-${v}, ${acc})`;
56
- }, fallback);
57
- };
52
+ export const v =
53
+ (prefix) =>
54
+ (...args) => {
55
+ const fallback = args.pop();
56
+ return args.reduceRight((acc, v) => {
57
+ return `var(--${prefix}-${v}, ${acc})`;
58
+ }, fallback);
59
+ };
58
60
 
59
61
  const pv = v('pie');
60
62
 
package/src/feedback.jsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { styled } from '@mui/material/styles';
4
- import { TransitionGroup, CSSTransition } from 'react-transition-group';
4
+ import { CSSTransition, TransitionGroup } from 'react-transition-group';
5
5
  import * as color from './color';
6
6
 
7
7
  const FeedbackContainer = styled('div')({
@@ -62,18 +62,10 @@ export class Feedback extends React.Component {
62
62
  if (!correctness || !feedback) return null;
63
63
 
64
64
  return (
65
- <CSSTransition
66
- key="hasFeedback"
67
- nodeRef={this.nodeRef}
68
- timeout={{ enter: 500, exit: 200 }}
69
- classNames="feedback"
70
- >
65
+ <CSSTransition key="hasFeedback" nodeRef={this.nodeRef} timeout={{ enter: 500, exit: 200 }} classNames="feedback">
71
66
  <TransitionWrapper ref={this.nodeRef}>
72
67
  <FeedbackContainer>
73
- <FeedbackContent
74
- className={correctness}
75
- dangerouslySetInnerHTML={{ __html: feedback }}
76
- />
68
+ <FeedbackContent className={correctness} dangerouslySetInnerHTML={{ __html: feedback }} />
77
69
  </FeedbackContainer>
78
70
  </TransitionWrapper>
79
71
  </CSSTransition>
@@ -83,9 +75,7 @@ export class Feedback extends React.Component {
83
75
  render() {
84
76
  return (
85
77
  <div>
86
- <TransitionGroup>
87
- {this.renderFeedback()}
88
- </TransitionGroup>
78
+ <TransitionGroup>{this.renderFeedback()}</TransitionGroup>
89
79
  </div>
90
80
  );
91
81
  }
@@ -12,7 +12,7 @@ const StyledFormControl = styled(FormControl)(({ theme }) => ({
12
12
  }));
13
13
 
14
14
  const StyledInputLabel = styled(InputLabel)(() => ({
15
- fontSize: 'inherit',
15
+ fontSize: 'inherit',
16
16
  whiteSpace: 'nowrap',
17
17
  margin: 0,
18
18
  padding: 0,
@@ -41,10 +41,7 @@ const InputContainer = ({ label, className, children }) => (
41
41
  InputContainer.propTypes = {
42
42
  label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
43
43
  className: PropTypes.string,
44
- children: PropTypes.oneOfType([
45
- PropTypes.arrayOf(PropTypes.node),
46
- PropTypes.node,
47
- ]).isRequired,
44
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
48
45
  };
49
46
 
50
47
  export default InputContainer;
@@ -26,7 +26,12 @@ class PreviewLayout extends React.Component {
26
26
  const accessibility = ariaLabel ? { 'aria-label': ariaLabel, role } : {};
27
27
 
28
28
  return (
29
- <StyledUiLayout {...accessibility} extraCSSRules={extraCSSRules} fontSizeFactor={fontSizeFactor} classes={classes}>
29
+ <StyledUiLayout
30
+ {...accessibility}
31
+ extraCSSRules={extraCSSRules}
32
+ fontSizeFactor={fontSizeFactor}
33
+ classes={classes}
34
+ >
30
35
  {children}
31
36
  </StyledUiLayout>
32
37
  );
package/src/ui-layout.jsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
- import { createTheme, ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
3
- import { styled } from '@mui/material/styles';
2
+ import { createTheme, styled, StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
4
3
  import PropTypes from 'prop-types';
5
4
 
6
5
  const theme = createTheme({
@@ -1 +0,0 @@
1
- []