@pie-lib/render-ui 7.0.3 → 8.0.0-next.20261002094057

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 (105) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +11 -0
  2. package/dist/assets/enableAudioAutoplayImage.d.ts +10 -0
  3. package/dist/assets/enableAudioAutoplayImage.js +4 -0
  4. package/dist/collapsible/index.d.ts +31 -0
  5. package/dist/collapsible/index.js +53 -0
  6. package/dist/color.d.ts +128 -0
  7. package/dist/color.js +132 -0
  8. package/dist/feedback.d.ts +20 -0
  9. package/dist/feedback.js +65 -0
  10. package/dist/has-media.d.ts +9 -0
  11. package/dist/has-media.js +10 -0
  12. package/dist/has-text.d.ts +9 -0
  13. package/dist/has-text.js +10 -0
  14. package/dist/html-and-math.d.ts +22 -0
  15. package/dist/html-and-math.js +25 -0
  16. package/dist/index.d.ts +26 -0
  17. package/dist/index.js +18 -0
  18. package/dist/inline-menu.d.ts +33 -0
  19. package/dist/inline-menu.js +29 -0
  20. package/dist/input-container.d.ts +23 -0
  21. package/dist/input-container.js +39 -0
  22. package/dist/preview-layout.d.ts +24 -0
  23. package/dist/preview-layout.js +37 -0
  24. package/dist/preview-prompt.d.ts +39 -0
  25. package/dist/preview-prompt.js +153 -0
  26. package/dist/purpose.d.ts +18 -0
  27. package/dist/purpose.js +11 -0
  28. package/dist/readable.d.ts +18 -0
  29. package/dist/readable.js +11 -0
  30. package/dist/response-indicators.d.ts +121 -0
  31. package/dist/response-indicators.js +62 -0
  32. package/{src/transform-headings.js → dist/transform-headings.d.ts} +9 -30
  33. package/dist/transform-headings.js +15 -0
  34. package/dist/ui-layout.d.ts +31 -0
  35. package/dist/ui-layout.js +55 -0
  36. package/dist/withUndoReset.d.ts +47 -0
  37. package/dist/withUndoReset.js +84 -0
  38. package/package.json +29 -20
  39. package/CHANGELOG.json +0 -1
  40. package/CHANGELOG.md +0 -1190
  41. package/LICENSE.md +0 -5
  42. package/lib/assets/enableAudioAutoplayImage.js +0 -8
  43. package/lib/assets/enableAudioAutoplayImage.js.map +0 -1
  44. package/lib/collapsible/index.js +0 -105
  45. package/lib/collapsible/index.js.map +0 -1
  46. package/lib/color.js +0 -253
  47. package/lib/color.js.map +0 -1
  48. package/lib/feedback.js +0 -112
  49. package/lib/feedback.js.map +0 -1
  50. package/lib/has-media.js +0 -22
  51. package/lib/has-media.js.map +0 -1
  52. package/lib/has-text.js +0 -22
  53. package/lib/has-text.js.map +0 -1
  54. package/lib/html-and-math.js +0 -54
  55. package/lib/html-and-math.js.map +0 -1
  56. package/lib/index.js +0 -113
  57. package/lib/index.js.map +0 -1
  58. package/lib/input-container.js +0 -59
  59. package/lib/input-container.js.map +0 -1
  60. package/lib/preview-layout.js +0 -65
  61. package/lib/preview-layout.js.map +0 -1
  62. package/lib/preview-prompt.js +0 -326
  63. package/lib/preview-prompt.js.map +0 -1
  64. package/lib/purpose.js +0 -22
  65. package/lib/purpose.js.map +0 -1
  66. package/lib/readable.js +0 -22
  67. package/lib/readable.js.map +0 -1
  68. package/lib/response-indicators.js +0 -102
  69. package/lib/response-indicators.js.map +0 -1
  70. package/lib/transform-headings.js +0 -53
  71. package/lib/transform-headings.js.map +0 -1
  72. package/lib/ui-layout.js +0 -125
  73. package/lib/ui-layout.js.map +0 -1
  74. package/lib/withUndoReset.js +0 -135
  75. package/lib/withUndoReset.js.map +0 -1
  76. package/src/__tests__/color.test.js +0 -259
  77. package/src/__tests__/feedback.test.jsx +0 -279
  78. package/src/__tests__/has-media.test.js +0 -19
  79. package/src/__tests__/has-text.test.js +0 -20
  80. package/src/__tests__/html-and-math.test.js +0 -36
  81. package/src/__tests__/input-container.test.jsx +0 -328
  82. package/src/__tests__/preview-layout.test.jsx +0 -349
  83. package/src/__tests__/preview-prompt.test.jsx +0 -573
  84. package/src/__tests__/purpose.test.jsx +0 -51
  85. package/src/__tests__/readable.test.jsx +0 -69
  86. package/src/__tests__/response-indicators.test.jsx +0 -111
  87. package/src/__tests__/ui-layout.test.jsx +0 -52
  88. package/src/__tests__/withUndoReset.test.jsx +0 -176
  89. package/src/assets/enableAudioAutoplayImage.js +0 -1
  90. package/src/collapsible/__tests__/index.test.jsx +0 -39
  91. package/src/collapsible/index.jsx +0 -64
  92. package/src/color.js +0 -151
  93. package/src/feedback.jsx +0 -84
  94. package/src/has-media.js +0 -16
  95. package/src/has-text.js +0 -18
  96. package/src/html-and-math.js +0 -21
  97. package/src/index.js +0 -35
  98. package/src/input-container.jsx +0 -47
  99. package/src/preview-layout.jsx +0 -41
  100. package/src/preview-prompt.jsx +0 -308
  101. package/src/purpose.jsx +0 -17
  102. package/src/readable.jsx +0 -19
  103. package/src/response-indicators.jsx +0 -82
  104. package/src/ui-layout.jsx +0 -96
  105. package/src/withUndoReset.jsx +0 -114
@@ -1,573 +0,0 @@
1
- import React from 'react';
2
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
- import userEvent from '@testing-library/user-event';
4
- import PreviewPrompt from '../preview-prompt';
5
- import * as color from '../color';
6
-
7
- jest.mock('@pie-lib/math-rendering', () => ({
8
- renderMath: jest.fn(),
9
- }));
10
-
11
- const { renderMath } = require('@pie-lib/math-rendering');
12
-
13
- describe('Prompt without Custom tag', () => {
14
- const defaultProps = {
15
- classes: {},
16
- prompt:
17
- 'Which of these northern European countries are EU members? <math><mstack><msrow><mn>111</mn></msrow><msline/></mstack></math>',
18
- tagName: '',
19
- className: '',
20
- };
21
-
22
- describe('default rendering with markup', () => {
23
- it('renders the prompt text', () => {
24
- render(<PreviewPrompt {...defaultProps} />);
25
- expect(screen.getByText(/Which of these northern European countries are EU members/)).toBeInTheDocument();
26
- });
27
-
28
- it('does not have prompt class by default', () => {
29
- const { container } = render(<PreviewPrompt {...defaultProps} />);
30
- expect(container.firstChild).not.toHaveClass('prompt');
31
- });
32
-
33
- it('renders math markup', () => {
34
- const { container } = render(<PreviewPrompt {...defaultProps} />);
35
- // Math may be rendered but transformed by the math rendering library
36
- // Just check that the prompt text is rendered
37
- expect(screen.getByText(/Which of these northern European countries are EU members/)).toBeInTheDocument();
38
- });
39
- });
40
- });
41
-
42
- describe('Prompt with Custom tag', () => {
43
- const defaultProps = {
44
- classes: {},
45
- prompt:
46
- 'Which of these northern European countries are EU members? <math><mstack><msrow><mn>111</mn></msrow><msline/></mstack></math>',
47
- tagName: 'span',
48
- className: 'prompt',
49
- };
50
-
51
- describe('renders with custom tag "span" correctly', () => {
52
- it('renders with custom className', () => {
53
- const { container } = render(<PreviewPrompt {...defaultProps} />);
54
- expect(container.firstChild).toHaveClass('prompt');
55
- });
56
-
57
- it('renders as span element when tagName is specified', () => {
58
- const { container } = render(<PreviewPrompt {...defaultProps} />);
59
- expect(container.firstChild.tagName.toLowerCase()).toBe('span');
60
- });
61
-
62
- it('renders the prompt text', () => {
63
- render(<PreviewPrompt {...defaultProps} />);
64
- expect(screen.getByText(/Which of these northern European countries are EU members/)).toBeInTheDocument();
65
- });
66
- });
67
- });
68
-
69
- describe('PreviewPrompt - Extended Tests', () => {
70
- beforeEach(() => {
71
- jest.clearAllMocks();
72
- document.body.innerHTML = '';
73
- });
74
-
75
- describe('tagName variations', () => {
76
- it('should render as legend when tagName is "legend"', () => {
77
- const { container } = render(<PreviewPrompt prompt="Legend text" tagName="legend" />);
78
- expect(container.firstChild.tagName.toLowerCase()).toBe('legend');
79
- });
80
-
81
- it('should render as div by default when no tagName', () => {
82
- const { container } = render(<PreviewPrompt prompt="Default text" />);
83
- expect(container.firstChild.tagName.toLowerCase()).toBe('div');
84
- });
85
-
86
- it('should apply legend class when tagName is "legend"', () => {
87
- const { container } = render(<PreviewPrompt prompt="Legend text" tagName="legend" />);
88
- expect(container.firstChild).toHaveClass('legend');
89
- });
90
-
91
- it('should render as p tag when specified', () => {
92
- const { container } = render(<PreviewPrompt prompt="Paragraph text" tagName="p" />);
93
- expect(container.firstChild.tagName.toLowerCase()).toBe('p');
94
- });
95
- });
96
-
97
- describe('className handling', () => {
98
- it('should apply single className', () => {
99
- const { container } = render(<PreviewPrompt prompt="Test" className="custom-class" />);
100
- expect(container.firstChild).toHaveClass('custom-class');
101
- });
102
-
103
- it('should apply defaultClassName', () => {
104
- const { container } = render(<PreviewPrompt prompt="Test" defaultClassName="default-class" />);
105
- expect(container.firstChild).toHaveClass('default-class');
106
- });
107
-
108
- it('should combine className and defaultClassName', () => {
109
- const { container } = render(<PreviewPrompt prompt="Test" className="custom" defaultClassName="default" />);
110
- expect(container.firstChild).toHaveClass('custom');
111
- expect(container.firstChild).toHaveClass('default');
112
- });
113
-
114
- it('should add legend class when tagName is legend', () => {
115
- const { container } = render(<PreviewPrompt prompt="Test" tagName="legend" className="custom" />);
116
- expect(container.firstChild).toHaveClass('custom');
117
- expect(container.firstChild).toHaveClass('legend');
118
- });
119
- });
120
-
121
- describe('onClick handler', () => {
122
- it('should call onClick when clicked', async () => {
123
- const onClick = jest.fn();
124
- const user = userEvent.setup();
125
-
126
- render(<PreviewPrompt prompt="Clickable text" onClick={onClick} />);
127
-
128
- await user.click(screen.getByText('Clickable text'));
129
- expect(onClick).toHaveBeenCalledTimes(1);
130
- });
131
-
132
- it('should work without onClick handler', async () => {
133
- const user = userEvent.setup();
134
-
135
- render(<PreviewPrompt prompt="Text without handler" />);
136
-
137
- await user.click(screen.getByText('Text without handler'));
138
- });
139
-
140
- it('should call onClick multiple times', async () => {
141
- const onClick = jest.fn();
142
- const user = userEvent.setup();
143
-
144
- render(<PreviewPrompt prompt="Multi-click text" onClick={onClick} />);
145
-
146
- await user.click(screen.getByText('Multi-click text'));
147
- await user.click(screen.getByText('Multi-click text'));
148
- await user.click(screen.getByText('Multi-click text'));
149
-
150
- expect(onClick).toHaveBeenCalledTimes(3);
151
- });
152
- });
153
-
154
- describe('newline replacement', () => {
155
- it('should replace \\embed{newLine}[] with \\newline', () => {
156
- const promptWithNewline = 'Line 1\\embed{newLine}[]Line 2';
157
- const { container } = render(<PreviewPrompt prompt={promptWithNewline} />);
158
-
159
- expect(container.innerHTML).toContain('\\newline');
160
- expect(container.innerHTML).not.toContain('\\embed{newLine}[]');
161
- });
162
-
163
- it('should replace multiple newlines', () => {
164
- const promptWithNewlines = 'A\\embed{newLine}[]B\\embed{newLine}[]C';
165
- const { container } = render(<PreviewPrompt prompt={promptWithNewlines} />);
166
-
167
- const newlineCount = (container.innerHTML.match(/\\newline/g) || []).length;
168
- expect(newlineCount).toBe(2);
169
- });
170
-
171
- it('should handle prompt without newlines', () => {
172
- const normalPrompt = 'Normal text without newlines';
173
- render(<PreviewPrompt prompt={normalPrompt} />);
174
-
175
- expect(screen.getByText('Normal text without newlines')).toBeInTheDocument();
176
- });
177
- });
178
-
179
- describe('audio handling', () => {
180
- it('should parse audio tag and add source element', () => {
181
- const promptWithAudio = '<audio src="test.mp3"></audio>';
182
- render(<PreviewPrompt prompt={promptWithAudio} />);
183
-
184
- const audio = document.querySelector('audio');
185
- expect(audio).toBeInTheDocument();
186
- expect(audio).toHaveAttribute('id', 'pie-prompt-audio-player');
187
-
188
- const source = audio.querySelector('source');
189
- expect(source).toBeInTheDocument();
190
- expect(source).toHaveAttribute('src', 'test.mp3');
191
- expect(source).toHaveAttribute('type', 'audio/mp3');
192
- });
193
-
194
- it('should remove src attribute from audio tag after adding source', () => {
195
- const promptWithAudio = '<audio src="test.mp3"></audio>';
196
- render(<PreviewPrompt prompt={promptWithAudio} />);
197
-
198
- const audio = document.querySelector('audio');
199
- expect(audio).not.toHaveAttribute('src');
200
- });
201
-
202
- it('should handle custom audio button', () => {
203
- const customAudioButton = {
204
- playImage: 'play.png',
205
- pauseImage: 'pause.png',
206
- };
207
- const promptWithAudio = '<audio src="test.mp3"></audio>';
208
-
209
- render(<PreviewPrompt prompt={promptWithAudio} customAudioButton={customAudioButton} />);
210
-
211
- const audio = document.getElementById('pie-prompt-audio-player');
212
- expect(audio).toHaveStyle({ display: 'none' });
213
-
214
- const playButton = document.getElementById('play-audio-button');
215
- expect(playButton).toBeInTheDocument();
216
- });
217
-
218
- it('should not add custom button when customAudioButton is not provided', () => {
219
- const promptWithAudio = '<audio src="test.mp3"></audio>';
220
- render(<PreviewPrompt prompt={promptWithAudio} />);
221
-
222
- const playButton = document.getElementById('play-audio-button');
223
- expect(playButton).not.toBeInTheDocument();
224
- });
225
- });
226
-
227
- // playImage is the idle state, pauseImage the playing one. The button has to
228
- // start in the state the audio is actually in: it used to always start on
229
- // pauseImage, and the click handler read that image back as "already
230
- // playing", so with autoplay off every click was silently swallowed.
231
- describe('custom audio button playback', () => {
232
- const customAudioButton = { playImage: 'play.png', pauseImage: 'pause.png' };
233
- const promptWithAudio = '<audio src="test.mp3"></audio>';
234
-
235
- let play;
236
- let paused;
237
-
238
- const stubPlayback = (impl) => {
239
- play = jest.fn(impl);
240
- // jsdom does not implement playback, and the component reads `paused`
241
- // alongside calling `play()`, so both are driven by hand here.
242
- Object.defineProperty(HTMLMediaElement.prototype, 'play', {
243
- configurable: true,
244
- writable: true,
245
- value: play,
246
- });
247
- Object.defineProperty(HTMLMediaElement.prototype, 'paused', {
248
- configurable: true,
249
- get: () => paused,
250
- });
251
- };
252
-
253
- const renderPrompt = (autoplayAudioEnabled) => {
254
- render(
255
- <PreviewPrompt
256
- prompt={promptWithAudio}
257
- autoplayAudioEnabled={autoplayAudioEnabled}
258
- customAudioButton={customAudioButton}
259
- />,
260
- );
261
-
262
- return document.getElementById('play-audio-button');
263
- };
264
-
265
- beforeEach(() => {
266
- paused = true;
267
- stubPlayback(() => Promise.resolve());
268
- });
269
-
270
- it('should start on the idle image when autoplay is off', () => {
271
- const playButton = renderPrompt(false);
272
-
273
- expect(playButton.style.backgroundImage).toContain(customAudioButton.playImage);
274
- });
275
-
276
- it('should play when clicked with autoplay off', () => {
277
- const playButton = renderPrompt(false);
278
-
279
- fireEvent.click(playButton);
280
-
281
- expect(play).toHaveBeenCalledTimes(1);
282
- });
283
-
284
- // No controller defaults `autoplayAudioEnabled`, so an item that does not
285
- // set it reaches delivery as undefined. The initial image tracks whether
286
- // play() is called - a truthy check - not whether the author opted out.
287
- it('should start on the idle image and stay clickable when autoplay is unset', () => {
288
- const playButton = renderPrompt(undefined);
289
-
290
- expect(playButton.style.backgroundImage).toContain(customAudioButton.playImage);
291
- expect(play).not.toHaveBeenCalled();
292
-
293
- fireEvent.click(playButton);
294
-
295
- expect(play).toHaveBeenCalledTimes(1);
296
- });
297
-
298
- it('should start on the playing image and autoplay when autoplay is on', () => {
299
- const playButton = renderPrompt(true);
300
-
301
- expect(playButton.style.backgroundImage).toContain(customAudioButton.pauseImage);
302
- expect(play).toHaveBeenCalledTimes(1);
303
- });
304
-
305
- it('should ignore a click while the audio is already playing', () => {
306
- const playButton = renderPrompt(false);
307
- paused = false;
308
-
309
- fireEvent.click(playButton);
310
-
311
- expect(play).not.toHaveBeenCalled();
312
- });
313
-
314
- it('should fall back to the idle image and stay clickable when autoplay is blocked', async () => {
315
- // Only the autoplay attempt is blocked; the click that follows is a user
316
- // gesture, so the browser allows it.
317
- let attempts = 0;
318
- stubPlayback(() => {
319
- attempts += 1;
320
- return attempts === 1 ? Promise.reject(new Error('NotAllowedError')) : Promise.resolve();
321
- });
322
- jest.spyOn(console, 'error').mockImplementation(() => {});
323
-
324
- const playButton = renderPrompt(true);
325
-
326
- await waitFor(() => {
327
- expect(playButton.style.backgroundImage).toContain(customAudioButton.playImage);
328
- });
329
-
330
- fireEvent.click(playButton);
331
-
332
- expect(play).toHaveBeenCalledTimes(2);
333
- });
334
- });
335
-
336
- describe('image alignment', () => {
337
- it('should align images with alignment attribute', () => {
338
- const promptWithImage = '<img src="test.jpg" alignment="center" />';
339
- render(<PreviewPrompt prompt={promptWithImage} />);
340
-
341
- const images = document.querySelectorAll('img');
342
- expect(images.length).toBeGreaterThan(0);
343
- });
344
-
345
- it('should handle images without alignment attribute', () => {
346
- const promptWithImage = '<img src="test.jpg" />';
347
- render(<PreviewPrompt prompt={promptWithImage} />);
348
-
349
- const img = document.querySelector('img');
350
- expect(img).toBeInTheDocument();
351
- });
352
- });
353
-
354
- describe('edge cases', () => {
355
- it('should handle empty prompt', () => {
356
- const { container } = render(<PreviewPrompt prompt="" />);
357
- expect(container.firstChild).toBeInTheDocument();
358
- });
359
-
360
- it('should handle undefined prompt', () => {
361
- const { container } = render(<PreviewPrompt prompt={undefined} />);
362
- expect(container.firstChild).toBeInTheDocument();
363
- });
364
-
365
- it('should handle null prompt', () => {
366
- const { container } = render(<PreviewPrompt prompt={null} />);
367
- expect(container.firstChild).toBeInTheDocument();
368
- });
369
-
370
- it('should handle very long prompt', () => {
371
- const longPrompt = 'A'.repeat(10000);
372
- render(<PreviewPrompt prompt={longPrompt} />);
373
- expect(screen.getByText(longPrompt)).toBeInTheDocument();
374
- });
375
-
376
- it('should handle prompt with special HTML characters', () => {
377
- const specialPrompt = '<div>Test & "quotes" \' apostrophe</div>';
378
- render(<PreviewPrompt prompt={specialPrompt} />);
379
- expect(screen.getByText(/Test/)).toBeInTheDocument();
380
- });
381
-
382
- it('should handle prompt with script tags safely', () => {
383
- const scriptPrompt = '<script>alert("XSS")</script>Safe text';
384
- const { container } = render(<PreviewPrompt prompt={scriptPrompt} />);
385
- expect(container).toBeInTheDocument();
386
- });
387
- });
388
-
389
- describe('dangerouslySetInnerHTML', () => {
390
- it('should render HTML using dangerouslySetInnerHTML', () => {
391
- const htmlPrompt = '<strong>Bold</strong> and <em>italic</em>';
392
- render(<PreviewPrompt prompt={htmlPrompt} />);
393
-
394
- expect(screen.getByText('Bold')).toBeInTheDocument();
395
- expect(screen.getByText(/italic/)).toBeInTheDocument();
396
- });
397
-
398
- it('should render complex nested HTML', () => {
399
- const complexPrompt = `
400
- <div>
401
- <h3>Title</h3>
402
- <p>Paragraph 1</p>
403
- <ul>
404
- <li>Item 1</li>
405
- <li>Item 2</li>
406
- </ul>
407
- </div>
408
- `;
409
- render(<PreviewPrompt prompt={complexPrompt} />);
410
-
411
- expect(screen.getByText('Title')).toBeInTheDocument();
412
- expect(screen.getByText('Paragraph 1')).toBeInTheDocument();
413
- expect(screen.getByText('Item 1')).toBeInTheDocument();
414
- expect(screen.getByText('Item 2')).toBeInTheDocument();
415
- });
416
-
417
- it('should render tables', () => {
418
- const tablePrompt = `
419
- <table>
420
- <tbody>
421
- <tr><th>Header</th><td>Data</td></tr>
422
- </tbody>
423
- </table>
424
- `;
425
- render(<PreviewPrompt prompt={tablePrompt} />);
426
-
427
- expect(screen.getByText('Header')).toBeInTheDocument();
428
- expect(screen.getByText('Data')).toBeInTheDocument();
429
- });
430
-
431
- // Asserted against the CSS emotion emitted rather than through toHaveStyle: jsdom
432
- // does not resolve custom properties, so it normalises `var()` away. Emotion
433
- // inserts through the CSSOM, so the <style> tags themselves carry no text.
434
- describe('authored table painting', () => {
435
- const emittedCss = () =>
436
- Array.from(document.querySelectorAll('style'))
437
- .flatMap((tag) => Array.from(tag.sheet?.cssRules || []))
438
- .map((rule) => rule.cssText)
439
- .join('\n');
440
-
441
- it('paints a bordered table flat, from the ink token', () => {
442
- render(<PreviewPrompt prompt={'<div><table border="1"><tbody><tr><td>A</td></tr></tbody></table></div>'} />);
443
-
444
- const css = emittedCss();
445
- expect(css).toContain('table[border]');
446
- expect(css).toContain('border-style: solid');
447
- expect(css).toContain(`border-color: ${color.tableGrid()}`);
448
- });
449
-
450
- it('takes the stripe from a scheme-following fill, not a fixed near-white one', () => {
451
- render(<PreviewPrompt prompt={'<table><tbody><tr><td>A</td></tr></tbody></table>'} />);
452
-
453
- const css = emittedCss();
454
- expect(css).toContain(`background-color: ${color.tableStripe()}`);
455
- // #f6f8fa still appears as the var() chain's last resort, so this checks that
456
- // nothing paints it bare - along with the `color: black` it used to be paired with.
457
- expect(css).not.toContain('background-color: #f6f8fa');
458
- expect(css).not.toContain('color: rgb(0, 0, 0)');
459
- });
460
- });
461
- });
462
-
463
- describe('component lifecycle', () => {
464
- it('should cleanup on unmount', () => {
465
- const { unmount } = render(<PreviewPrompt prompt="Test" />);
466
- unmount();
467
- // Should not throw errors on unmount
468
- });
469
-
470
- it('should update when props change', () => {
471
- const { rerender } = render(<PreviewPrompt prompt="Initial" />);
472
- rerender(<PreviewPrompt prompt="Updated" />);
473
-
474
- expect(screen.getByText('Updated')).toBeInTheDocument();
475
- expect(screen.queryByText('Initial')).not.toBeInTheDocument();
476
- });
477
-
478
- it('should handle rapid prop changes', () => {
479
- const { rerender } = render(<PreviewPrompt prompt="V1" />);
480
- rerender(<PreviewPrompt prompt="V2" />);
481
- rerender(<PreviewPrompt prompt="V3" />);
482
- rerender(<PreviewPrompt prompt="V4" />);
483
-
484
- expect(screen.getByText('V4')).toBeInTheDocument();
485
- });
486
- });
487
-
488
- describe('accessibility', () => {
489
- it('should mark the container with the stable preview-prompt class', () => {
490
- const { container } = render(<PreviewPrompt prompt="Accessible text" />);
491
- expect(container.firstChild).toHaveClass('preview-prompt');
492
- });
493
-
494
- it('should not emit an id on the container', () => {
495
- const { container } = render(<PreviewPrompt prompt="Accessible text" />);
496
- expect(container.firstChild).not.toHaveAttribute('id');
497
- });
498
-
499
- it('should leave no duplicate ids when a page renders many prompts', () => {
500
- const { container } = render(
501
- <div>
502
- <PreviewPrompt prompt="Item 1" />
503
- <PreviewPrompt prompt="Item 2" />
504
- <PreviewPrompt prompt="Item 3" />
505
- <PreviewPrompt prompt="Item 4" />
506
- <PreviewPrompt prompt="Item 5" />
507
- </div>,
508
- );
509
-
510
- const ids = Array.from(container.querySelectorAll('[id]')).map((el) => el.id);
511
-
512
- expect(ids).toEqual(Array.from(new Set(ids)));
513
- });
514
-
515
- it('should preserve semantic HTML', () => {
516
- const semanticPrompt = '<strong>Important:</strong> Read carefully';
517
- render(<PreviewPrompt prompt={semanticPrompt} />);
518
-
519
- const strong = document.querySelector('strong');
520
- expect(strong).toBeInTheDocument();
521
- expect(strong).toHaveTextContent('Important:');
522
- });
523
- });
524
-
525
- describe('multiple instances', () => {
526
- it('should handle multiple PreviewPrompt components', () => {
527
- const { container } = render(
528
- <div>
529
- <PreviewPrompt prompt="First prompt" />
530
- <PreviewPrompt prompt="Second prompt" />
531
- <PreviewPrompt prompt="Third prompt" />
532
- </div>,
533
- );
534
-
535
- expect(screen.getByText('First prompt')).toBeInTheDocument();
536
- expect(screen.getByText('Second prompt')).toBeInTheDocument();
537
- expect(screen.getByText('Third prompt')).toBeInTheDocument();
538
- });
539
-
540
- it('should typeset each instance with its own node rather than the first prompt on the page', () => {
541
- renderMath.mockClear();
542
-
543
- render(
544
- <div>
545
- <PreviewPrompt prompt="First prompt" />
546
- <PreviewPrompt prompt="Second prompt" />
547
- <PreviewPrompt prompt="Third prompt" />
548
- </div>,
549
- );
550
-
551
- const typesetNodes = renderMath.mock.calls.map(([node]) => node);
552
-
553
- expect(typesetNodes).toHaveLength(3);
554
- expect(new Set(typesetNodes).size).toBe(3);
555
- expect(typesetNodes.map((node) => node.textContent)).toEqual(['First prompt', 'Second prompt', 'Third prompt']);
556
- });
557
-
558
- it('should align images within each instance independently', () => {
559
- const { container } = render(
560
- <div>
561
- <PreviewPrompt prompt='<img src="a.png" alignment="center" />' />
562
- <PreviewPrompt prompt='<img src="b.png" alignment="right" />' />
563
- </div>,
564
- );
565
-
566
- const wrappers = container.querySelectorAll('.preview-prompt > div');
567
-
568
- expect(wrappers).toHaveLength(2);
569
- expect(wrappers[0].style.justifyContent).toBe('center');
570
- expect(wrappers[1].style.justifyContent).toBe('flex-end');
571
- });
572
- });
573
- });
@@ -1,51 +0,0 @@
1
- import React from 'react';
2
- import { render, screen } from '@testing-library/react';
3
- import Purpose from '../purpose';
4
-
5
- describe('Purpose', () => {
6
- describe('rendering', () => {
7
- it('renders child with data-pie-purpose attribute even when purpose prop is not provided', () => {
8
- const { container } = render(
9
- <Purpose>
10
- <div>text</div>
11
- </Purpose>,
12
- );
13
-
14
- expect(screen.getByText('text')).toBeInTheDocument();
15
- const div = container.querySelector('div');
16
- // When purpose is undefined, React sets the attribute as data-pie-purpose="undefined"
17
- // This is expected behavior - the component always adds the attribute
18
- expect(div).toBeInTheDocument();
19
- });
20
-
21
- it('renders child with data-pie-purpose="passage"', () => {
22
- const { container } = render(
23
- <Purpose purpose="passage">
24
- <div>text</div>
25
- </Purpose>,
26
- );
27
-
28
- expect(screen.getByText('text')).toBeInTheDocument();
29
- const div = container.querySelector('div');
30
- expect(div).toHaveAttribute('data-pie-purpose', 'passage');
31
- });
32
-
33
- it('renders multiple children with data-pie-purpose attribute', () => {
34
- const { container } = render(
35
- <Purpose purpose="something">
36
- <div>
37
- <div>text1</div>
38
- <div>text2</div>
39
- </div>
40
- </Purpose>,
41
- );
42
-
43
- expect(screen.getByText('text1')).toBeInTheDocument();
44
- expect(screen.getByText('text2')).toBeInTheDocument();
45
- expect(screen.queryByText('text3')).not.toBeInTheDocument();
46
-
47
- const parentDiv = container.querySelector('div');
48
- expect(parentDiv).toHaveAttribute('data-pie-purpose', 'something');
49
- });
50
- });
51
- });
@@ -1,69 +0,0 @@
1
- import React from 'react';
2
- import { render, screen } from '@testing-library/react';
3
- import Readable from '../readable';
4
-
5
- describe('Readable', () => {
6
- describe('rendering', () => {
7
- it('renders child with data-pie-readable attribute set to true', () => {
8
- const { container } = render(
9
- <Readable>
10
- <div>text</div>
11
- </Readable>,
12
- );
13
-
14
- expect(screen.getByText('text')).toBeInTheDocument();
15
- const div = container.querySelector('div');
16
- expect(div).toHaveAttribute('data-pie-readable', 'true');
17
- });
18
-
19
- it('renders multiple children with data-pie-readable attribute', () => {
20
- const { container } = render(
21
- <Readable>
22
- <div>
23
- <div>text1</div>
24
- <div>text2</div>
25
- </div>
26
- </Readable>,
27
- );
28
-
29
- expect(screen.getByText('text1')).toBeInTheDocument();
30
- expect(screen.getByText('text2')).toBeInTheDocument();
31
- expect(screen.queryByText('text3')).not.toBeInTheDocument();
32
-
33
- const parentDiv = container.querySelector('div');
34
- expect(parentDiv).toHaveAttribute('data-pie-readable', 'true');
35
- });
36
-
37
- it('renders with data-pie-readable set to false when false prop is provided', () => {
38
- const { container } = render(
39
- <Readable false>
40
- <div>
41
- <div>text1</div>
42
- <div>text2</div>
43
- </div>
44
- </Readable>,
45
- );
46
-
47
- expect(screen.getByText('text1')).toBeInTheDocument();
48
- expect(screen.getByText('text2')).toBeInTheDocument();
49
- expect(screen.queryByText('text3')).not.toBeInTheDocument();
50
-
51
- const parentDiv = container.querySelector('div');
52
- expect(parentDiv).toHaveAttribute('data-pie-readable', 'false');
53
- });
54
-
55
- it('renders with data-pie-readable set to false when false={true}', () => {
56
- const { container } = render(
57
- <Readable false={true}>
58
- <div>
59
- <div>text1</div>
60
- <div>text2</div>
61
- </div>
62
- </Readable>,
63
- );
64
-
65
- const parentDiv = container.querySelector('div');
66
- expect(parentDiv).toHaveAttribute('data-pie-readable', 'false');
67
- });
68
- });
69
- });