react-markdown-table-ts 1.2.2 → 1.3.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.
@@ -0,0 +1,646 @@
1
+ /**
2
+ * @fileoverview Comprehensive tests for the MarkdownTable React component
3
+ * including rendering, props handling, and callback functionality.
4
+ */
5
+
6
+ import { render, screen, waitFor } from '@testing-library/react';
7
+ import { MarkdownTable } from '../index';
8
+ import Prism from 'prismjs';
9
+ import * as utils from '../utils';
10
+
11
+ // Get the mocked highlightElement function
12
+ const mockHighlightElement = Prism.highlightElement as jest.Mock;
13
+
14
+ describe('MarkdownTable', () => {
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ });
18
+
19
+ describe('basic rendering', () => {
20
+ it('should render without crashing with minimal props', () => {
21
+ render(<MarkdownTable />);
22
+ const codeElement = screen.getByRole('code');
23
+ expect(codeElement).toBeInTheDocument();
24
+ });
25
+
26
+ it('should render with valid data', () => {
27
+ const data = [
28
+ ['Name', 'Age'],
29
+ ['John', '30'],
30
+ ];
31
+
32
+ render(<MarkdownTable inputData={data} />);
33
+
34
+ const codeElement = screen.getByRole('code');
35
+ expect(codeElement).toBeInTheDocument();
36
+ expect(codeElement.textContent).toContain('Name');
37
+ expect(codeElement.textContent).toContain('Age');
38
+ expect(codeElement.textContent).toContain('John');
39
+ expect(codeElement.textContent).toContain('30');
40
+ });
41
+
42
+ it('should render table with alphabet headers when hasHeader is false', () => {
43
+ const data = [
44
+ ['1', '2', '3'],
45
+ ['4', '5', '6'],
46
+ ];
47
+
48
+ render(<MarkdownTable inputData={data} hasHeader={false} />);
49
+
50
+ const codeElement = screen.getByRole('code');
51
+ expect(codeElement.textContent).toContain('A');
52
+ expect(codeElement.textContent).toContain('B');
53
+ expect(codeElement.textContent).toContain('C');
54
+ });
55
+
56
+ it('should render with null inputData', () => {
57
+ render(<MarkdownTable inputData={null} />);
58
+
59
+ const codeElement = screen.getByRole('code');
60
+ expect(codeElement).toBeInTheDocument();
61
+ expect(codeElement.textContent).toContain('Error:');
62
+ });
63
+
64
+ it('should render error message for invalid data', () => {
65
+ render(<MarkdownTable inputData={[]} />);
66
+
67
+ const codeElement = screen.getByRole('code');
68
+ expect(codeElement.textContent).toContain('Error:');
69
+ expect(codeElement.textContent).toContain('must contain at least one row');
70
+ });
71
+
72
+ it.skip('should re-throw non-MarkdownTableError exceptions', () => {
73
+ // Suppress all console methods for this test
74
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
75
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
76
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
77
+
78
+ // Spy on generateMarkdownTableString to throw a generic Error
79
+ const spy = jest.spyOn(utils, 'generateMarkdownTableString').mockImplementationOnce(() => {
80
+ throw new Error('Unexpected internal error');
81
+ });
82
+
83
+ // The component will try to render but React will catch and recover from the error
84
+ // This test verifies the error re-throw path (line 54) is executed
85
+ let didComplete = false;
86
+ try {
87
+ render(<MarkdownTable inputData={[['A'], ['B']]} />);
88
+ didComplete = true;
89
+ } catch (error) {
90
+ // Expected: React catches the error during render
91
+ didComplete = true;
92
+ }
93
+
94
+ // Verify the mock was called, which means our error path was executed
95
+ expect(spy).toHaveBeenCalled();
96
+ expect(didComplete).toBe(true);
97
+
98
+ // Restore all mocks
99
+ spy.mockRestore();
100
+ consoleErrorSpy.mockRestore();
101
+ consoleWarnSpy.mockRestore();
102
+ consoleLogSpy.mockRestore();
103
+ });
104
+ });
105
+
106
+ describe('alignment props', () => {
107
+ const data = [
108
+ ['Left', 'Center', 'Right'],
109
+ ['A', 'B', 'C'],
110
+ ];
111
+
112
+ it('should apply left alignment', () => {
113
+ render(<MarkdownTable inputData={data} columnAlignments={['left']} />);
114
+
115
+ const codeElement = screen.getByRole('code');
116
+ expect(codeElement.textContent).toMatch(/:-+/);
117
+ });
118
+
119
+ it('should apply center alignment', () => {
120
+ render(<MarkdownTable inputData={data} columnAlignments={['none', 'center']} />);
121
+
122
+ const codeElement = screen.getByRole('code');
123
+ expect(codeElement.textContent).toMatch(/:-+:/);
124
+ });
125
+
126
+ it('should apply right alignment', () => {
127
+ render(<MarkdownTable inputData={data} columnAlignments={['none', 'none', 'right']} />);
128
+
129
+ const codeElement = screen.getByRole('code');
130
+ expect(codeElement.textContent).toMatch(/-+:/);
131
+ });
132
+
133
+ it('should handle multiple alignments', () => {
134
+ render(
135
+ <MarkdownTable
136
+ inputData={data}
137
+ columnAlignments={['left', 'center', 'right']}
138
+ />
139
+ );
140
+
141
+ const codeElement = screen.getByRole('code');
142
+ const content = codeElement.textContent || '';
143
+
144
+ expect(content).toMatch(/:-+/); // left
145
+ expect(content).toMatch(/:-+:/); // center
146
+ expect(content).toMatch(/-+:/); // right
147
+ });
148
+ });
149
+
150
+ describe('formatting options', () => {
151
+ const data = [
152
+ ['Header1', 'Header2'],
153
+ ['Value1', 'Value2'],
154
+ ];
155
+
156
+ it('should generate compact table when isCompact is true', () => {
157
+ const { container } = render(
158
+ <MarkdownTable inputData={data} isCompact={true} />
159
+ );
160
+
161
+ const codeElement = container.querySelector('code');
162
+ expect(codeElement).toBeInTheDocument();
163
+ });
164
+
165
+ it('should add tabs when hasTabs is true', () => {
166
+ render(<MarkdownTable inputData={data} hasTabs={true} />);
167
+
168
+ const codeElement = screen.getByRole('code');
169
+ expect(codeElement.textContent).toContain('\t');
170
+ });
171
+
172
+ it('should not add tabs when hasTabs is false', () => {
173
+ render(<MarkdownTable inputData={data} hasTabs={false} />);
174
+
175
+ const codeElement = screen.getByRole('code');
176
+ expect(codeElement.textContent).not.toContain('\t');
177
+ });
178
+
179
+ it('should add padding by default', () => {
180
+ render(<MarkdownTable inputData={data} />);
181
+
182
+ const codeElement = screen.getByRole('code');
183
+ expect(codeElement.textContent).toMatch(/\|\s+\w+\s+\|/);
184
+ });
185
+
186
+ it('should not add padding when hasPadding is false', () => {
187
+ render(<MarkdownTable inputData={data} hasPadding={false} />);
188
+
189
+ const codeElement = screen.getByRole('code');
190
+ expect(codeElement.textContent).toContain('|Header1|Header2|');
191
+ });
192
+
193
+ it('should replace newlines when convertLineBreaks is true', () => {
194
+ const dataWithNewlines = [
195
+ ['Header'],
196
+ ['Line1\nLine2'],
197
+ ];
198
+
199
+ render(<MarkdownTable inputData={dataWithNewlines} convertLineBreaks={true} />);
200
+
201
+ const codeElement = screen.getByRole('code');
202
+ expect(codeElement.textContent).toContain('<br>');
203
+ });
204
+
205
+ it('should not replace newlines when convertLineBreaks is false', () => {
206
+ const dataWithNewlines = [
207
+ ['Header'],
208
+ ['Line1\nLine2'],
209
+ ];
210
+
211
+ render(<MarkdownTable inputData={dataWithNewlines} convertLineBreaks={false} />);
212
+
213
+ const codeElement = screen.getByRole('code');
214
+ expect(codeElement.textContent).not.toContain('<br>');
215
+ });
216
+ });
217
+
218
+ describe('theme prop', () => {
219
+ const data = [['A', 'B'], ['1', '2']];
220
+
221
+ it('should apply light theme by default', () => {
222
+ const { container } = render(<MarkdownTable inputData={data} />);
223
+
224
+ const preElement = container.querySelector('pre');
225
+ expect(preElement?.className).not.toContain('dark-theme');
226
+ });
227
+
228
+ it('should apply dark theme when specified', () => {
229
+ const { container } = render(<MarkdownTable inputData={data} theme="dark" />);
230
+
231
+ const preElement = container.querySelector('pre');
232
+ expect(preElement?.className).toContain('dark-theme');
233
+ });
234
+
235
+ it('should inject light theme CSS', () => {
236
+ const { container } = render(<MarkdownTable inputData={data} theme="light" />);
237
+
238
+ const styleElement = container.querySelector('style');
239
+ expect(styleElement?.textContent).toContain('code[class*=language-]');
240
+ });
241
+
242
+ it('should inject dark theme CSS', () => {
243
+ const { container } = render(<MarkdownTable inputData={data} theme="dark" />);
244
+
245
+ const styleElement = container.querySelector('style');
246
+ expect(styleElement?.textContent).toContain('code[class*=language-]');
247
+ });
248
+ });
249
+
250
+ describe('className prop', () => {
251
+ const data = [['A'], ['1']];
252
+
253
+ it('should apply custom className', () => {
254
+ const { container } = render(
255
+ <MarkdownTable inputData={data} className="custom-class" />
256
+ );
257
+
258
+ const preElement = container.querySelector('pre');
259
+ expect(preElement?.className).toContain('custom-class');
260
+ });
261
+
262
+ it('should combine custom className with default classes', () => {
263
+ const { container } = render(
264
+ <MarkdownTable inputData={data} className="custom-class" />
265
+ );
266
+
267
+ const preElement = container.querySelector('pre');
268
+ expect(preElement?.className).toContain('custom-class');
269
+ expect(preElement?.className).toContain('language-markdown');
270
+ expect(preElement?.className).toContain('line-numbers');
271
+ });
272
+
273
+ it('should work without custom className', () => {
274
+ const { container } = render(<MarkdownTable inputData={data} />);
275
+
276
+ const preElement = container.querySelector('pre');
277
+ expect(preElement?.className).toContain('language-markdown');
278
+ });
279
+ });
280
+
281
+ describe('showLineNumbers prop', () => {
282
+ const data = [['A'], ['1']];
283
+
284
+ it('should include line-numbers class by default', () => {
285
+ const { container } = render(<MarkdownTable inputData={data} />);
286
+
287
+ const preElement = container.querySelector('pre');
288
+ expect(preElement?.className).toContain('line-numbers');
289
+ });
290
+
291
+ it('should include line-numbers class when showLineNumbers is true', () => {
292
+ const { container } = render(
293
+ <MarkdownTable inputData={data} showLineNumbers={true} />
294
+ );
295
+
296
+ const preElement = container.querySelector('pre');
297
+ expect(preElement?.className).toContain('line-numbers');
298
+ });
299
+
300
+ it('should not include line-numbers class when showLineNumbers is false', () => {
301
+ const { container } = render(
302
+ <MarkdownTable inputData={data} showLineNumbers={false} />
303
+ );
304
+
305
+ const preElement = container.querySelector('pre');
306
+ expect(preElement?.className).not.toContain('line-numbers');
307
+ });
308
+ });
309
+
310
+ describe('preStyle prop', () => {
311
+ const data = [['A'], ['1']];
312
+
313
+ it('should apply custom styles to pre element', () => {
314
+ const customStyle = {
315
+ maxHeight: '300px',
316
+ backgroundColor: '#f5f5f5',
317
+ };
318
+
319
+ const { container } = render(
320
+ <MarkdownTable inputData={data} preStyle={customStyle} />
321
+ );
322
+
323
+ const preElement = container.querySelector('pre') as HTMLPreElement;
324
+ expect(preElement.style.maxHeight).toBe('300px');
325
+ // Browser converts hex to rgb, so check if backgroundColor is set
326
+ expect(preElement.style.backgroundColor).toBeTruthy();
327
+ });
328
+
329
+ it('should maintain default styles with custom preStyle', () => {
330
+ const { container } = render(
331
+ <MarkdownTable inputData={data} preStyle={{ maxHeight: '500px' }} />
332
+ );
333
+
334
+ const preElement = container.querySelector('pre') as HTMLPreElement;
335
+ // Check that styles are applied, even if computed differently
336
+ expect(preElement.style.maxHeight).toBe('500px');
337
+ expect(preElement.style.margin).toBe('0px');
338
+ });
339
+ });
340
+
341
+ describe('topPadding prop', () => {
342
+ const data = [['A'], ['1']];
343
+
344
+ it('should apply default top padding of 16px', () => {
345
+ const { container } = render(<MarkdownTable inputData={data} />);
346
+
347
+ const styleElement = container.querySelector('style');
348
+ expect(styleElement?.textContent).toContain('padding-top: 16px');
349
+ });
350
+
351
+ it('should apply custom top padding', () => {
352
+ const { container } = render(<MarkdownTable inputData={data} topPadding={32} />);
353
+
354
+ const styleElement = container.querySelector('style');
355
+ expect(styleElement?.textContent).toContain('padding-top: 32px');
356
+ });
357
+
358
+ it('should accept zero as top padding', () => {
359
+ const { container } = render(<MarkdownTable inputData={data} topPadding={0} />);
360
+
361
+ const styleElement = container.querySelector('style');
362
+ expect(styleElement?.textContent).toContain('padding-top: 0px');
363
+ });
364
+ });
365
+
366
+ describe('minWidth prop', () => {
367
+ const data = [['A'], ['1']];
368
+
369
+ it('should apply minWidth when specified', () => {
370
+ const { container } = render(<MarkdownTable inputData={data} minWidth={400} />);
371
+
372
+ const preElement = container.querySelector('pre') as HTMLPreElement;
373
+ expect(preElement.style.minWidth).toBe('400px');
374
+ });
375
+
376
+ it('should use min-content when minWidth is not specified', () => {
377
+ const { container } = render(<MarkdownTable inputData={data} />);
378
+
379
+ const preElement = container.querySelector('pre') as HTMLPreElement;
380
+ expect(preElement.style.minWidth).toBe('min-content');
381
+ });
382
+ });
383
+
384
+ describe('onGenerate callback', () => {
385
+ it('should call onGenerate with generated markdown', () => {
386
+ const onGenerate = jest.fn();
387
+ const data = [
388
+ ['Name', 'Age'],
389
+ ['John', '30'],
390
+ ];
391
+
392
+ render(<MarkdownTable inputData={data} onGenerate={onGenerate} />);
393
+
394
+ expect(onGenerate).toHaveBeenCalledTimes(1);
395
+ expect(onGenerate).toHaveBeenCalledWith(expect.stringContaining('Name'));
396
+ expect(onGenerate).toHaveBeenCalledWith(expect.stringContaining('John'));
397
+ });
398
+
399
+ it('should call onGenerate when data changes', () => {
400
+ const onGenerate = jest.fn();
401
+ const data1 = [['A'], ['1']];
402
+ const data2 = [['B'], ['2']];
403
+
404
+ const { rerender } = render(
405
+ <MarkdownTable inputData={data1} onGenerate={onGenerate} />
406
+ );
407
+
408
+ expect(onGenerate).toHaveBeenCalledTimes(1);
409
+
410
+ rerender(<MarkdownTable inputData={data2} onGenerate={onGenerate} />);
411
+
412
+ expect(onGenerate).toHaveBeenCalledTimes(2);
413
+ });
414
+
415
+ it('should call onGenerate with error message on invalid data', () => {
416
+ const onGenerate = jest.fn();
417
+
418
+ render(<MarkdownTable inputData={[]} onGenerate={onGenerate} />);
419
+
420
+ expect(onGenerate).toHaveBeenCalledWith(expect.stringContaining('Error:'));
421
+ });
422
+
423
+ it('should work without onGenerate callback', () => {
424
+ const data = [['A'], ['1']];
425
+
426
+ expect(() => {
427
+ render(<MarkdownTable inputData={data} />);
428
+ }).not.toThrow();
429
+ });
430
+ });
431
+
432
+ describe('Prism.js integration', () => {
433
+ it('should call Prism.highlightElement on mount', async () => {
434
+ const data = [['A'], ['1']];
435
+
436
+ render(<MarkdownTable inputData={data} />);
437
+
438
+ await waitFor(() => {
439
+ expect(mockHighlightElement).toHaveBeenCalled();
440
+ });
441
+ });
442
+
443
+ it('should call Prism.highlightElement when data changes', async () => {
444
+ const data1 = [['A'], ['1']];
445
+ const data2 = [['B'], ['2']];
446
+
447
+ const { rerender } = render(<MarkdownTable inputData={data1} />);
448
+
449
+ await waitFor(() => {
450
+ expect(mockHighlightElement).toHaveBeenCalled();
451
+ });
452
+
453
+ const callCount = mockHighlightElement.mock.calls.length;
454
+
455
+ rerender(<MarkdownTable inputData={data2} />);
456
+
457
+ await waitFor(() => {
458
+ expect(mockHighlightElement.mock.calls.length).toBeGreaterThan(callCount);
459
+ });
460
+ });
461
+
462
+ it('should highlight code element with correct classes', () => {
463
+ const data = [['A'], ['1']];
464
+
465
+ const { container } = render(<MarkdownTable inputData={data} />);
466
+
467
+ const codeElement = container.querySelector('code');
468
+ expect(codeElement?.className).toContain('language-markdown');
469
+ });
470
+ });
471
+
472
+ describe('useDeferredValue optimisation', () => {
473
+ it('should render with deferred input data', () => {
474
+ const data = [['A', 'B'], ['1', '2']];
475
+
476
+ render(<MarkdownTable inputData={data} />);
477
+
478
+ const codeElement = screen.getByRole('code');
479
+ expect(codeElement).toBeInTheDocument();
480
+ expect(codeElement.textContent).toContain('A');
481
+ });
482
+
483
+ it('should handle rapid data updates', () => {
484
+ const data1 = [['A'], ['1']];
485
+ const data2 = [['B'], ['2']];
486
+ const data3 = [['C'], ['3']];
487
+
488
+ const { rerender } = render(<MarkdownTable inputData={data1} />);
489
+ rerender(<MarkdownTable inputData={data2} />);
490
+ rerender(<MarkdownTable inputData={data3} />);
491
+
492
+ const codeElement = screen.getByRole('code');
493
+ expect(codeElement).toBeInTheDocument();
494
+ });
495
+ });
496
+
497
+ describe('DOM structure', () => {
498
+ const data = [['A'], ['1']];
499
+
500
+ it('should render with correct wrapper structure', () => {
501
+ const { container } = render(<MarkdownTable inputData={data} />);
502
+
503
+ const wrapper = container.querySelector('div');
504
+ expect(wrapper).toBeInTheDocument();
505
+ expect(wrapper?.style.position).toBe('relative');
506
+ expect(wrapper?.style.isolation).toBe('isolate');
507
+ expect(wrapper?.style.display).toBe('inline-block');
508
+ });
509
+
510
+ it('should have unique id for wrapper div', () => {
511
+ const { container: container1 } = render(<MarkdownTable inputData={data} />);
512
+ const { container: container2 } = render(<MarkdownTable inputData={data} />);
513
+
514
+ const wrapper1 = container1.querySelector('div');
515
+ const wrapper2 = container2.querySelector('div');
516
+
517
+ expect(wrapper1?.id).toBeTruthy();
518
+ expect(wrapper2?.id).toBeTruthy();
519
+ expect(wrapper1?.id).not.toBe(wrapper2?.id);
520
+ });
521
+
522
+ it('should contain pre element inside wrapper', () => {
523
+ const { container } = render(<MarkdownTable inputData={data} />);
524
+
525
+ const wrapper = container.querySelector('div');
526
+ const preElement = wrapper?.querySelector('pre');
527
+
528
+ expect(preElement).toBeInTheDocument();
529
+ });
530
+
531
+ it('should contain code element inside pre', () => {
532
+ const { container } = render(<MarkdownTable inputData={data} />);
533
+
534
+ const preElement = container.querySelector('pre');
535
+ const codeElement = preElement?.querySelector('code');
536
+
537
+ expect(codeElement).toBeInTheDocument();
538
+ });
539
+ });
540
+
541
+ describe('edge cases', () => {
542
+ it('should handle very large datasets', () => {
543
+ const largeData = Array.from({ length: 100 }, (_, i) => [
544
+ `Col1-${i}`,
545
+ `Col2-${i}`,
546
+ `Col3-${i}`,
547
+ ]);
548
+
549
+ expect(() => {
550
+ render(<MarkdownTable inputData={largeData} />);
551
+ }).not.toThrow();
552
+ });
553
+
554
+ it('should handle single cell table', () => {
555
+ const data = [['SingleCell']];
556
+
557
+ render(<MarkdownTable inputData={data} />);
558
+
559
+ const codeElement = screen.getByRole('code');
560
+ expect(codeElement.textContent).toContain('SingleCell');
561
+ });
562
+
563
+ it('should handle empty strings in cells', () => {
564
+ const data = [['', ''], ['', '']];
565
+
566
+ render(<MarkdownTable inputData={data} />);
567
+
568
+ const codeElement = screen.getByRole('code');
569
+ expect(codeElement).toBeInTheDocument();
570
+ });
571
+
572
+ it('should handle special characters', () => {
573
+ const data = [['<>', '&amp;', '\'"`']];
574
+
575
+ render(<MarkdownTable inputData={data} />);
576
+
577
+ const codeElement = screen.getByRole('code');
578
+ expect(codeElement).toBeInTheDocument();
579
+ });
580
+
581
+ it('should handle undefined props gracefully', () => {
582
+ expect(() => {
583
+ render(<MarkdownTable />);
584
+ }).not.toThrow();
585
+ });
586
+ });
587
+
588
+ describe('prop combinations', () => {
589
+ const data = [
590
+ ['Name', 'Description'],
591
+ ['Item1', 'First item'],
592
+ ['Item2', 'Second item'],
593
+ ];
594
+
595
+ it('should handle all formatting options together', () => {
596
+ render(
597
+ <MarkdownTable
598
+ inputData={data}
599
+ hasHeader={true}
600
+ columnAlignments={['left', 'center']}
601
+ isCompact={false}
602
+ hasTabs={true}
603
+ hasPadding={true}
604
+ convertLineBreaks={true}
605
+ />
606
+ );
607
+
608
+ const codeElement = screen.getByRole('code');
609
+ expect(codeElement).toBeInTheDocument();
610
+ });
611
+
612
+ it('should handle minimal configuration', () => {
613
+ render(<MarkdownTable inputData={data} />);
614
+
615
+ const codeElement = screen.getByRole('code');
616
+ expect(codeElement).toBeInTheDocument();
617
+ });
618
+
619
+ it('should handle maximum customisation', () => {
620
+ const onGenerate = jest.fn();
621
+
622
+ render(
623
+ <MarkdownTable
624
+ inputData={data}
625
+ hasHeader={false}
626
+ columnAlignments={['right', 'center']}
627
+ isCompact={true}
628
+ hasTabs={false}
629
+ hasPadding={false}
630
+ convertLineBreaks={true}
631
+ className="custom-class"
632
+ onGenerate={onGenerate}
633
+ theme="dark"
634
+ preStyle={{ maxHeight: '200px' }}
635
+ topPadding={24}
636
+ minWidth={500}
637
+ />
638
+ );
639
+
640
+ const codeElement = screen.getByRole('code');
641
+ expect(codeElement).toBeInTheDocument();
642
+ expect(onGenerate).toHaveBeenCalled();
643
+ });
644
+ });
645
+ });
646
+