@adobe/spacecat-shared-html-analyzer 1.0.7 → 1.2.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,644 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { expect } from 'chai';
14
+ import * as cheerio from 'cheerio';
15
+ import {
16
+ htmlToMarkdown,
17
+ markdownToHtml,
18
+ htmlToMarkdownToHtml,
19
+ diffDOMBlocks,
20
+ createMarkdownTableDiff,
21
+ generateMarkdownDiff,
22
+ htmlToRenderedMarkdown,
23
+ } from '../src/index.js';
24
+
25
+ describe('Markdown Conversion and Diff', () => {
26
+ describe('htmlToMarkdown', () => {
27
+ it('should convert HTML to Markdown', async () => {
28
+ const html = '<h1>Title</h1><p>This is <strong>bold</strong> text.</p>';
29
+ const markdown = await htmlToMarkdown(html);
30
+
31
+ expect(markdown).to.be.a('string');
32
+ expect(markdown).to.include('Title');
33
+ expect(markdown).to.include('bold');
34
+ });
35
+
36
+ it('should handle empty HTML', async () => {
37
+ const result = await htmlToMarkdown('');
38
+ expect(result).to.equal('');
39
+ });
40
+
41
+ it('should handle null/undefined input', async () => {
42
+ expect(await htmlToMarkdown(null)).to.equal('');
43
+ expect(await htmlToMarkdown(undefined)).to.equal('');
44
+ });
45
+ });
46
+
47
+ describe('markdownToHtml', () => {
48
+ it('should convert Markdown to HTML', async () => {
49
+ const markdown = '# Title\n\nThis is **bold** text.';
50
+ const html = await markdownToHtml(markdown);
51
+
52
+ expect(html).to.be.a('string');
53
+ expect(html).to.include('Title');
54
+ expect(html).to.include('bold');
55
+ });
56
+
57
+ it('should handle empty markdown', async () => {
58
+ const result = await markdownToHtml('');
59
+ expect(result).to.equal('');
60
+ });
61
+
62
+ it('should handle null/undefined input', async () => {
63
+ expect(await markdownToHtml(null)).to.equal('');
64
+ expect(await markdownToHtml(undefined)).to.equal('');
65
+ });
66
+ });
67
+
68
+ describe('htmlToMarkdownToHtml', () => {
69
+ it('should normalize HTML through markdown', async () => {
70
+ const html = '<div><h1>Title</h1><p>Content</p></div>';
71
+ const result = await htmlToMarkdownToHtml(html);
72
+
73
+ expect(result).to.be.a('string');
74
+ expect(result).to.include('Title');
75
+ expect(result).to.include('Content');
76
+ });
77
+ });
78
+
79
+ describe('htmlToRenderedMarkdown', () => {
80
+ it('should convert HTML to rendered markdown HTML', async () => {
81
+ const html = '<html><body><h1>Title</h1><p>Content</p></body></html>';
82
+ const result = await htmlToRenderedMarkdown(html, true);
83
+
84
+ expect(result).to.be.a('string');
85
+ expect(result).to.include('Title');
86
+ expect(result).to.include('Content');
87
+ });
88
+
89
+ it('should respect ignoreNavFooter option', async () => {
90
+ const html = '<html><body><nav>Nav</nav><h1>Title</h1><footer>Footer</footer></body></html>';
91
+ const resultWithFilter = await htmlToRenderedMarkdown(html, true);
92
+ const resultWithoutFilter = await htmlToRenderedMarkdown(html, false);
93
+
94
+ expect(resultWithFilter).to.include('Title');
95
+ expect(resultWithoutFilter).to.include('Title');
96
+ });
97
+ });
98
+
99
+ describe('generateMarkdownDiff', () => {
100
+ it('should generate rendered HTML for both sides', async () => {
101
+ const originalHtml = '<html><body><h1>Title</h1><p>Original</p></body></html>';
102
+ const currentHtml = '<html><body><h1>Title</h1><p>Updated</p></body></html>';
103
+
104
+ const result = await generateMarkdownDiff(originalHtml, currentHtml);
105
+
106
+ expect(result).to.have.property('originalRenderedHtml');
107
+ expect(result).to.have.property('currentRenderedHtml');
108
+ expect(result.originalRenderedHtml).to.be.a('string');
109
+ expect(result.currentRenderedHtml).to.be.a('string');
110
+ });
111
+
112
+ it('should respect ignoreNavFooter option', async () => {
113
+ const html = '<html><body><nav>Nav</nav><h1>Title</h1><footer>Footer</footer></body></html>';
114
+
115
+ const resultIgnored = await generateMarkdownDiff(html, html, true);
116
+ const resultNotIgnored = await generateMarkdownDiff(html, html, false);
117
+
118
+ expect(resultIgnored.originalRenderedHtml).to.be.a('string');
119
+ expect(resultNotIgnored.originalRenderedHtml).to.be.a('string');
120
+ });
121
+ });
122
+
123
+ describe('diffDOMBlocks', () => {
124
+ it('should find matching blocks with identical text', () => {
125
+ const originalBlocks = [
126
+ { html: '<p>Hello World</p>', text: 'Hello World', tagName: 'p' },
127
+ { html: '<p>Goodbye</p>', text: 'Goodbye', tagName: 'p' },
128
+ ];
129
+ const currentBlocks = [
130
+ { html: '<p>Hello World</p>', text: 'Hello World', tagName: 'p' },
131
+ { html: '<p>Goodbye</p>', text: 'Goodbye', tagName: 'p' },
132
+ ];
133
+
134
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
135
+
136
+ expect(ops).to.have.lengthOf(2);
137
+ expect(ops[0].type).to.equal('same');
138
+ expect(ops[1].type).to.equal('same');
139
+ });
140
+
141
+ it('should detect additions', () => {
142
+ const originalBlocks = [
143
+ { html: '<p>Hello</p>', text: 'Hello', tagName: 'p' },
144
+ ];
145
+ const currentBlocks = [
146
+ { html: '<p>Hello</p>', text: 'Hello', tagName: 'p' },
147
+ { html: '<p>New Content</p>', text: 'New Content', tagName: 'p' },
148
+ ];
149
+
150
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
151
+
152
+ expect(ops).to.have.lengthOf(2);
153
+ expect(ops[0].type).to.equal('same');
154
+ expect(ops[1].type).to.equal('add');
155
+ expect(ops[1].currentBlock.text).to.equal('New Content');
156
+ });
157
+
158
+ it('should detect deletions', () => {
159
+ const originalBlocks = [
160
+ { html: '<p>Hello</p>', text: 'Hello', tagName: 'p' },
161
+ { html: '<p>Removed</p>', text: 'Removed', tagName: 'p' },
162
+ ];
163
+ const currentBlocks = [
164
+ { html: '<p>Hello</p>', text: 'Hello', tagName: 'p' },
165
+ ];
166
+
167
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
168
+
169
+ expect(ops).to.have.lengthOf(2);
170
+ expect(ops[0].type).to.equal('same');
171
+ expect(ops[1].type).to.equal('del');
172
+ expect(ops[1].originalBlock.text).to.equal('Removed');
173
+ });
174
+
175
+ it('should handle empty blocks', () => {
176
+ const originalBlocks = [];
177
+ const currentBlocks = [];
178
+
179
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
180
+
181
+ expect(ops).to.have.lengthOf(0);
182
+ });
183
+
184
+ it('should match blocks by text content regardless of HTML structure', () => {
185
+ const originalBlocks = [
186
+ { html: '<p>Content</p>', text: 'Content', tagName: 'p' },
187
+ ];
188
+ const currentBlocks = [
189
+ { html: '<div><p>Content</p></div>', text: 'Content', tagName: 'p' },
190
+ ];
191
+
192
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
193
+
194
+ expect(ops).to.have.lengthOf(1);
195
+ expect(ops[0].type).to.equal('same');
196
+ });
197
+
198
+ it('should handle mixed operations (add, delete, same)', () => {
199
+ const originalBlocks = [
200
+ { html: '<p>Keep</p>', text: 'Keep', tagName: 'p' },
201
+ { html: '<p>Delete</p>', text: 'Delete', tagName: 'p' },
202
+ ];
203
+ const currentBlocks = [
204
+ { html: '<p>Keep</p>', text: 'Keep', tagName: 'p' },
205
+ { html: '<p>Add</p>', text: 'Add', tagName: 'p' },
206
+ ];
207
+
208
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
209
+
210
+ const sameOps = ops.filter((op) => op.type === 'same');
211
+ const delOps = ops.filter((op) => op.type === 'del');
212
+ const addOps = ops.filter((op) => op.type === 'add');
213
+
214
+ expect(sameOps).to.have.lengthOf(1);
215
+ expect(delOps).to.have.lengthOf(1);
216
+ expect(addOps).to.have.lengthOf(1);
217
+ });
218
+
219
+ it('should use LCS algorithm to find optimal matching', () => {
220
+ const originalBlocks = [
221
+ { html: '<p>A</p>', text: 'A', tagName: 'p' },
222
+ { html: '<p>B</p>', text: 'B', tagName: 'p' },
223
+ { html: '<p>C</p>', text: 'C', tagName: 'p' },
224
+ ];
225
+ const currentBlocks = [
226
+ { html: '<p>A</p>', text: 'A', tagName: 'p' },
227
+ { html: '<p>C</p>', text: 'C', tagName: 'p' },
228
+ ];
229
+
230
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
231
+
232
+ expect(ops[0].type).to.equal('same');
233
+ expect(ops[1].type).to.equal('del');
234
+ expect(ops[2].type).to.equal('same');
235
+ });
236
+
237
+ it('should handle large number of blocks efficiently', function () {
238
+ this.timeout(5000);
239
+
240
+ const blocks = Array.from({ length: 100 }, (_, i) => ({
241
+ html: `<p>Block ${i}</p>`,
242
+ text: `Block ${i}`,
243
+ tagName: 'p',
244
+ }));
245
+
246
+ const start = Date.now();
247
+ const ops = diffDOMBlocks(blocks, blocks);
248
+ const duration = Date.now() - start;
249
+
250
+ expect(ops).to.have.lengthOf(100);
251
+ expect(duration).to.be.lessThan(1000);
252
+ });
253
+ });
254
+
255
+ describe('createMarkdownTableDiff - Node.js (Cheerio)', () => {
256
+ // Helper to recursively create element structure for testing
257
+ const createElementFromCheerio = ($, el) => {
258
+ const children = $(el).children().toArray();
259
+ return {
260
+ tagName: el.tagName?.toUpperCase() || 'DIV',
261
+ outerHTML: $.html(el),
262
+ textContent: $(el).text(),
263
+ get children() {
264
+ return children.map((child) => createElementFromCheerio($, child));
265
+ },
266
+ };
267
+ };
268
+
269
+ const createElementsFromHtml = (html) => {
270
+ const $ = cheerio.load(html);
271
+ return $('body').children().toArray().map((el) => createElementFromCheerio($, el));
272
+ };
273
+
274
+ it('should extract simple list items as individual blocks', () => {
275
+ const originalHtml = '<body><p>Item 1</p><p>Item 2</p></body>';
276
+ const currentHtml = '<body><ul><li>Item 1</li><li>Item 2</li></ul></body>';
277
+
278
+ const originalChildren = createElementsFromHtml(originalHtml);
279
+ const currentChildren = createElementsFromHtml(currentHtml);
280
+
281
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
282
+
283
+ expect(result.tableHtml).to.be.a('string');
284
+ expect(result.counters).to.be.a('string');
285
+ expect(result.counters).to.include('No differences');
286
+ });
287
+
288
+ it('should extract nested paragraphs from list items', () => {
289
+ const originalHtml = '<body><p>Question</p><p>Answer</p></body>';
290
+ const currentHtml = '<body><ul><li><p>Question</p><p>Answer</p></li></ul></body>';
291
+
292
+ const originalChildren = createElementsFromHtml(originalHtml);
293
+ const currentChildren = createElementsFromHtml(currentHtml);
294
+
295
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
296
+
297
+ expect(result.counters).to.include('No differences');
298
+ });
299
+
300
+ it('should handle mixed list and non-list elements', () => {
301
+ const originalHtml = '<body><h1>Title</h1><p>Text</p></body>';
302
+ const currentHtml = '<body><h1>Title</h1><ul><li>Text</li></ul></body>';
303
+
304
+ const originalChildren = createElementsFromHtml(originalHtml);
305
+ const currentChildren = createElementsFromHtml(currentHtml);
306
+
307
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
308
+
309
+ expect(result.tableHtml).to.include('Title');
310
+ expect(result.tableHtml).to.include('Text');
311
+ });
312
+
313
+ it('should preserve list styling with ul wrapper', () => {
314
+ const originalHtml = '<body><p>Item</p></body>';
315
+ const currentHtml = '<body><ul><li>Item</li></ul></body>';
316
+
317
+ const originalChildren = createElementsFromHtml(originalHtml);
318
+ const currentChildren = createElementsFromHtml(currentHtml);
319
+
320
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
321
+
322
+ expect(result.tableHtml).to.include('<ul>');
323
+ expect(result.tableHtml).to.include('<li>');
324
+ });
325
+
326
+ it('should preserve list styling with ol wrapper', () => {
327
+ const originalHtml = '<body><p>First</p></body>';
328
+ const currentHtml = '<body><ol><li>First</li></ol></body>';
329
+
330
+ const originalChildren = createElementsFromHtml(originalHtml);
331
+ const currentChildren = createElementsFromHtml(currentHtml);
332
+
333
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
334
+
335
+ expect(result.tableHtml).to.include('<ol>');
336
+ expect(result.tableHtml).to.include('<li>');
337
+ });
338
+
339
+ it('should handle list items with multiple nested block elements', () => {
340
+ const originalHtml = '<body><p>Q</p><p>A</p><p>Detail</p></body>';
341
+ const currentHtml = '<body><ul><li><p>Q</p><p>A</p><p>Detail</p></li></ul></body>';
342
+
343
+ const originalChildren = createElementsFromHtml(originalHtml);
344
+ const currentChildren = createElementsFromHtml(currentHtml);
345
+
346
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
347
+
348
+ expect(result.counters).to.include('No differences');
349
+ });
350
+
351
+ it('should handle complex FAQ structure', () => {
352
+ const originalHtml = `
353
+ <body>
354
+ <p>FAQs</p>
355
+ <p>What are the new prices?</p>
356
+ <p>The prices have been revised.</p>
357
+ </body>
358
+ `;
359
+ const currentHtml = `
360
+ <body>
361
+ <h2>FAQs</h2>
362
+ <ul>
363
+ <li>
364
+ <p>What are the new prices?</p>
365
+ <p>The prices have been revised.</p>
366
+ </li>
367
+ </ul>
368
+ </body>
369
+ `;
370
+
371
+ const originalChildren = createElementsFromHtml(originalHtml);
372
+ const currentChildren = createElementsFromHtml(currentHtml);
373
+
374
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
375
+
376
+ expect(result.tableHtml).to.include('FAQs');
377
+ expect(result.tableHtml).to.include('What are the new prices');
378
+ expect(result.tableHtml).to.include('prices have been revised');
379
+ });
380
+
381
+ it('should count additions and deletions correctly', () => {
382
+ const originalHtml = '<body><p>Keep</p><p>Delete</p></body>';
383
+ const currentHtml = '<body><ul><li><p>Keep</p></li><li><p>Add</p></li></ul></body>';
384
+
385
+ const originalChildren = createElementsFromHtml(originalHtml);
386
+ const currentChildren = createElementsFromHtml(currentHtml);
387
+
388
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
389
+
390
+ expect(result.counters).to.include('addition');
391
+ expect(result.counters).to.include('deletion');
392
+ });
393
+
394
+ it('should handle deeply nested list structures', () => {
395
+ const complexHtml = `
396
+ <body>
397
+ <ul>
398
+ <li><p>Item 1</p><p>Sub 1</p></li>
399
+ <li><p>Item 2</p><p>Sub 2</p></li>
400
+ <li><p>Item 3</p><p>Sub 3</p></li>
401
+ </ul>
402
+ </body>
403
+ `;
404
+
405
+ const children = createElementsFromHtml(complexHtml);
406
+
407
+ const start = Date.now();
408
+ const result = createMarkdownTableDiff(children, children);
409
+ const duration = Date.now() - start;
410
+
411
+ expect(result.counters).to.include('No differences');
412
+ expect(duration).to.be.lessThan(500);
413
+ });
414
+
415
+ it('should filter out empty list items to prevent misalignment', () => {
416
+ const originalHtml = `
417
+ <body>
418
+ <h2>PRODUCT CATEGORY</h2>
419
+ <ul>
420
+ <li>Item 1</li>
421
+ <li></li>
422
+ <li>Item 2</li>
423
+ </ul>
424
+ <h2>NEW DROPS</h2>
425
+ </body>
426
+ `;
427
+ const currentHtml = `
428
+ <body>
429
+ <h2>PRODUCT CATEGORY</h2>
430
+ <ul>
431
+ <li>Item 1</li>
432
+ <li></li>
433
+ <li>Item 2</li>
434
+ </ul>
435
+ <h2>NEW DROPS</h2>
436
+ </body>
437
+ `;
438
+
439
+ const originalChildren = createElementsFromHtml(originalHtml);
440
+ const currentChildren = createElementsFromHtml(currentHtml);
441
+
442
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
443
+
444
+ // Should recognize as no differences despite empty list items
445
+ expect(result.counters).to.include('No differences');
446
+ // Should not show NEW DROPS as changed
447
+ expect(result.tableHtml).to.not.include('diff-line-del');
448
+ expect(result.tableHtml).to.not.include('diff-line-add');
449
+ });
450
+
451
+ it('should handle duplicate headings at different positions correctly', () => {
452
+ const originalHtml = `
453
+ <body>
454
+ <h2>PRODUCT CATEGORY</h2>
455
+ <ul>
456
+ <li>Item 1</li>
457
+ <li>Item 2</li>
458
+ <li>Item 3</li>
459
+ </ul>
460
+ <h2>NEW DROPS</h2>
461
+ </body>
462
+ `;
463
+ const currentHtml = `
464
+ <body>
465
+ <h2>PRODUCT CATEGORY</h2>
466
+ <ul>
467
+ <li>Item 1</li>
468
+ </ul>
469
+ <h2>NEW DROPS</h2>
470
+ </body>
471
+ `;
472
+
473
+ const originalChildren = createElementsFromHtml(originalHtml);
474
+ const currentChildren = createElementsFromHtml(currentHtml);
475
+
476
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
477
+
478
+ // Should show list items as deleted but heading should match
479
+ expect(result.counters).to.include('deletion');
480
+ // NEW DROPS heading should appear as "same" on both sides
481
+ const { tableHtml } = result;
482
+ const newDropsMatches = (tableHtml.match(/NEW DROPS/g) || []).length;
483
+ // Should appear twice (once in each column as "same")
484
+ expect(newDropsMatches).to.be.at.least(2);
485
+ });
486
+
487
+ it('should handle lists with only empty items', () => {
488
+ const originalHtml = `
489
+ <body>
490
+ <h2>Title</h2>
491
+ <ul>
492
+ <li></li>
493
+ <li></li>
494
+ </ul>
495
+ <p>After list</p>
496
+ </body>
497
+ `;
498
+ const currentHtml = `
499
+ <body>
500
+ <h2>Title</h2>
501
+ <ul>
502
+ <li></li>
503
+ <li></li>
504
+ </ul>
505
+ <p>After list</p>
506
+ </body>
507
+ `;
508
+
509
+ const originalChildren = createElementsFromHtml(originalHtml);
510
+ const currentChildren = createElementsFromHtml(currentHtml);
511
+
512
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
513
+
514
+ // Should match correctly without treating empty items as blocks
515
+ expect(result.counters).to.include('No differences');
516
+ });
517
+
518
+ it('should handle mixed empty and whitespace-only list items', () => {
519
+ const originalHtml = `
520
+ <body>
521
+ <ul>
522
+ <li>Valid Item</li>
523
+ <li></li>
524
+ <li> </li>
525
+ <li>Another Valid</li>
526
+ </ul>
527
+ </body>
528
+ `;
529
+ const currentHtml = `
530
+ <body>
531
+ <ul>
532
+ <li>Valid Item</li>
533
+ <li></li>
534
+ <li> </li>
535
+ <li>Another Valid</li>
536
+ </ul>
537
+ </body>
538
+ `;
539
+
540
+ const originalChildren = createElementsFromHtml(originalHtml);
541
+ const currentChildren = createElementsFromHtml(currentHtml);
542
+
543
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
544
+
545
+ // Should only compare non-empty items
546
+ expect(result.counters).to.include('No differences');
547
+ });
548
+
549
+ it('should handle nested empty blocks within list items', () => {
550
+ const originalHtml = `
551
+ <body>
552
+ <ul>
553
+ <li><p>Content</p><p></p></li>
554
+ <li><p></p><p>More Content</p></li>
555
+ </ul>
556
+ </body>
557
+ `;
558
+ const currentHtml = `
559
+ <body>
560
+ <ul>
561
+ <li><p>Content</p><p></p></li>
562
+ <li><p></p><p>More Content</p></li>
563
+ </ul>
564
+ </body>
565
+ `;
566
+
567
+ const originalChildren = createElementsFromHtml(originalHtml);
568
+ const currentChildren = createElementsFromHtml(currentHtml);
569
+
570
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
571
+
572
+ // Should filter empty nested blocks
573
+ expect(result.counters).to.include('No differences');
574
+ });
575
+
576
+ it('should correctly align when list sizes differ significantly', () => {
577
+ const originalHtml = `
578
+ <body>
579
+ <h2>Section A</h2>
580
+ <ul>
581
+ <li>Item 1</li>
582
+ <li>Item 2</li>
583
+ <li>Item 3</li>
584
+ <li>Item 4</li>
585
+ <li>Item 5</li>
586
+ </ul>
587
+ <h2>Section B</h2>
588
+ </body>
589
+ `;
590
+ const currentHtml = `
591
+ <body>
592
+ <h2>Section A</h2>
593
+ <ul>
594
+ <li>Item 1</li>
595
+ <li>Item 2</li>
596
+ </ul>
597
+ <h2>Section B</h2>
598
+ </body>
599
+ `;
600
+
601
+ const originalChildren = createElementsFromHtml(originalHtml);
602
+ const currentChildren = createElementsFromHtml(currentHtml);
603
+
604
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
605
+
606
+ // Section headings should match correctly
607
+ expect(result.tableHtml).to.include('Section A');
608
+ expect(result.tableHtml).to.include('Section B');
609
+ // Should show 3 deletions (Item 3, 4, 5)
610
+ expect(result.counters).to.include('3 block deletion');
611
+ });
612
+
613
+ it('should handle empty HTML input', () => {
614
+ const result = createMarkdownTableDiff([], []);
615
+
616
+ expect(result.tableHtml).to.be.a('string');
617
+ expect(result.counters).to.include('No differences');
618
+ });
619
+
620
+ it('should handle unicode characters', () => {
621
+ const originalHtml = '<body><p>Hello 世界 🌍</p></body>';
622
+ const currentHtml = '<body><ul><li>Hello 世界 🌍</li></ul></body>';
623
+
624
+ const originalChildren = createElementsFromHtml(originalHtml);
625
+ const currentChildren = createElementsFromHtml(currentHtml);
626
+
627
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
628
+
629
+ expect(result.counters).to.include('No differences');
630
+ });
631
+
632
+ it('should handle whitespace-only differences', () => {
633
+ const originalHtml = '<body><p>Text</p></body>';
634
+ const currentHtml = '<body><p> Text </p></body>';
635
+
636
+ const originalChildren = createElementsFromHtml(originalHtml);
637
+ const currentChildren = createElementsFromHtml(currentHtml);
638
+
639
+ const result = createMarkdownTableDiff(originalChildren, currentChildren);
640
+
641
+ expect(result.counters).to.include('No differences');
642
+ });
643
+ });
644
+ });