@elisra-devops/docgen-skins 0.22.0 → 0.24.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 (55) hide show
  1. package/.github/workflows/ci.yml +18 -9
  2. package/bin/index.d.ts +8 -1
  3. package/bin/index.js +86 -37
  4. package/bin/index.js.map +1 -1
  5. package/bin/models/json/JSONRun.js.map +1 -1
  6. package/bin/models/json/file/JSONFile.js.map +1 -1
  7. package/bin/models/json/paragraph/JSONParagraph.js.map +1 -1
  8. package/bin/models/json/paragraph/JSONRichTextParagraph.js.map +1 -1
  9. package/bin/models/json/paragraph/JSONTestSuiteHeaderParagraph.js.map +1 -1
  10. package/bin/models/json/table/JSONTable.d.ts +2 -1
  11. package/bin/models/json/table/JSONTable.js +20 -6
  12. package/bin/models/json/table/JSONTable.js.map +1 -1
  13. package/bin/models/json/table/JSONTableCell.js.map +1 -1
  14. package/bin/models/json/table/JSONTableRow.js.map +1 -1
  15. package/bin/services/logger.js +17 -7
  16. package/bin/services/logger.js.map +1 -1
  17. package/bin/test/JSONFile.test.js +48 -0
  18. package/bin/test/JSONFile.test.js.map +1 -0
  19. package/bin/test/JSONRun.test.d.ts +1 -0
  20. package/bin/test/JSONRun.test.js +93 -0
  21. package/bin/test/JSONRun.test.js.map +1 -0
  22. package/bin/test/JSONTable.test.d.ts +1 -0
  23. package/bin/test/JSONTable.test.js +79 -0
  24. package/bin/test/JSONTable.test.js.map +1 -0
  25. package/bin/test/JSONTableCell.test.d.ts +1 -0
  26. package/bin/test/JSONTableCell.test.js +129 -0
  27. package/bin/test/JSONTableCell.test.js.map +1 -0
  28. package/bin/test/JSONTableRow.test.d.ts +1 -0
  29. package/bin/test/JSONTableRow.test.js +40 -0
  30. package/bin/test/JSONTableRow.test.js.map +1 -0
  31. package/bin/test/JSONTestSuiteHeaderParagraph.test.d.ts +1 -0
  32. package/bin/test/JSONTestSuiteHeaderParagraph.test.js +39 -0
  33. package/bin/test/JSONTestSuiteHeaderParagraph.test.js.map +1 -0
  34. package/bin/test/SkinsEdgeCases.test.d.ts +1 -0
  35. package/bin/test/SkinsEdgeCases.test.js +399 -0
  36. package/bin/test/SkinsEdgeCases.test.js.map +1 -0
  37. package/bin/test/integration/skins.integration.test.d.ts +1 -0
  38. package/bin/test/integration/skins.integration.test.js +492 -0
  39. package/bin/test/integration/skins.integration.test.js.map +1 -0
  40. package/package.json +5 -5
  41. package/src/index.ts +94 -41
  42. package/src/models/json/table/JSONTable.ts +19 -3
  43. package/src/test/JSONFile.test.ts +59 -0
  44. package/src/test/JSONRun.test.ts +118 -0
  45. package/src/test/JSONTable.test.ts +126 -0
  46. package/src/test/JSONTableCell.test.ts +150 -0
  47. package/src/test/JSONTableRow.test.ts +42 -0
  48. package/src/test/JSONTestSuiteHeaderParagraph.test.ts +40 -0
  49. package/src/test/SkinsEdgeCases.test.ts +530 -0
  50. package/src/test/integration/skins.integration.test.ts +608 -0
  51. package/tsconfig.json +2 -2
  52. package/bin/test/skins.test.js +0 -126
  53. package/bin/test/skins.test.js.map +0 -1
  54. package/src/test/skins.test.ts +0 -189
  55. /package/bin/test/{skins.test.d.ts → JSONFile.test.d.ts} +0 -0
package/src/index.ts CHANGED
@@ -20,7 +20,11 @@ export default class Skins {
20
20
  SKIN_TYPE_TRACE = 'trace-table';
21
21
  SKIN_TYPE_TABLE_STR = 'str-table';
22
22
  SKIN_TYPE_PARAGRAPH = 'paragraph';
23
+ SKIN_TYPE_COVER_PAGE = 'cover-page';
23
24
  SKIN_TYPE_TEST_PLAN = 'test-plan';
25
+ SKIN_TYPE_TEST_STD = 'test-std';
26
+ SKIN_TYPE_TEST_STP = 'test-stp';
27
+ SKIN_TYPE_TEST_STR = 'test-str';
24
28
  SKIN_TYPE_SYSTEM_OVERVIEW = 'system-overview';
25
29
  SKIN_TYPE_INSTALLATION = 'installation';
26
30
  SKIN_TYPE_TEST_REPORTER = 'test-reporter';
@@ -37,6 +41,16 @@ export default class Skins {
37
41
  this.skinFormat = skinFormat;
38
42
  }
39
43
 
44
+ private resolveSkinTypeAlias(skinType: string) {
45
+ if (skinType === this.SKIN_TYPE_TEST_STD || skinType === this.SKIN_TYPE_TEST_STP) {
46
+ return this.SKIN_TYPE_TEST_PLAN;
47
+ }
48
+ if (skinType === this.SKIN_TYPE_TEST_STR) {
49
+ return this.SKIN_TYPE_TABLE_STR;
50
+ }
51
+ return skinType;
52
+ }
53
+
40
54
  async addNewContentToDocumentSkin(
41
55
  contentControlTitle: string,
42
56
  skinType: string,
@@ -50,8 +64,10 @@ export default class Skins {
50
64
  ): Promise<any[]> {
51
65
  try {
52
66
  let populatedSkin: any[] = [];
67
+ const isStpTestSkin = skinType === this.SKIN_TYPE_TEST_STP;
68
+ const normalizedSkinType = this.resolveSkinTypeAlias(skinType);
53
69
 
54
- switch (skinType) {
70
+ switch (normalizedSkinType) {
55
71
  case this.SKIN_TYPE_TABLE:
56
72
  populatedSkin = this.generateQueryBasedTable(data, headerStyles, styles, headingLvl);
57
73
 
@@ -73,6 +89,9 @@ export default class Skins {
73
89
  case this.SKIN_TYPE_PARAGRAPH:
74
90
  populatedSkin = this.generateQueryBasedParagraphs(data, styles, headingLvl);
75
91
  break;
92
+ case this.SKIN_TYPE_COVER_PAGE:
93
+ populatedSkin = this.generateCoverPageParagraphs(data, styles, headingLvl);
94
+ break;
76
95
  case this.SKIN_TYPE_TEST_PLAN:
77
96
  populatedSkin = this.generateTestBasedSkin(
78
97
  data,
@@ -80,7 +99,8 @@ export default class Skins {
80
99
  styles,
81
100
  headingLvl,
82
101
  includeAttachments,
83
- isFlattened
102
+ isFlattened,
103
+ isStpTestSkin ? 1 : 0
84
104
  );
85
105
  break;
86
106
  case this.SKIN_TYPE_SYSTEM_OVERVIEW:
@@ -327,49 +347,63 @@ export default class Skins {
327
347
  }
328
348
  }
329
349
 
350
+ private generateParagraphsFromData(data: any, styles: StyleOptions, headingLvl: number = 0): any[] {
351
+ const paragraphs: any[] = [];
352
+
353
+ data.forEach((wi: WIData) => {
354
+ wi.fields.forEach((field: WIProperty) => {
355
+ // Description and Test Description are handled via rich-text handler
356
+ if (field.name === 'Description: ' || field.name === 'Test Description:') {
357
+ const paragraphSkin = new JSONRichTextParagraph(field, styles, wi.Source, headingLvl + wi.level);
358
+ paragraphs.push(paragraphSkin.getJSONRichTextParagraph());
359
+ }
360
+
361
+ // Exclude ID / Description / Test Description from the normal paragraph path
362
+ if (field.name !== 'ID' && field.name !== 'Description: ' && field.name !== 'Test Description:') {
363
+ const paragraphSkin = new JSONParagraph(field, styles, wi.Source, headingLvl + wi.level);
364
+ paragraphs.push(paragraphSkin.getJSONParagraph());
365
+ }
366
+ });
367
+ });
368
+
369
+ return paragraphs;
370
+ }
371
+
372
+ generateCoverPageParagraphs(data: any, styles: StyleOptions, headingLvl: number = 0): any[] {
373
+ logger.debug(`Generating cover-page paragraphs as ${this.skinFormat}`);
374
+ try {
375
+ switch (this.skinFormat) {
376
+ case 'json':
377
+ return this.generateParagraphsFromData(data, styles, headingLvl);
378
+ case 'html':
379
+ logger.info(`Generating html cover-page paragraphs!`);
380
+ return [];
381
+ default:
382
+ throw new Error(`Invalid skin format ${this.skinFormat}`);
383
+ }
384
+ } catch (error: any) {
385
+ logger.error(`Error occurred in generateCoverPageParagraphs: ${error.message}`);
386
+ throw error;
387
+ }
388
+ }
389
+
330
390
  generateQueryBasedParagraphs(data: any, styles: StyleOptions, headingLvl: number = 0): any[] {
331
391
  logger.debug(`Generating paragraph as ${this.skinFormat}`);
332
392
  try {
333
393
  switch (this.skinFormat) {
334
394
  case 'json':
335
- let paragraphs: any[] = [];
336
- data.forEach((wi: WIData) => {
337
- wi.fields.forEach((field: WIProperty) => {
338
- //Description and Test Description are handled in richText handler
339
- if (field.name === 'Description: ' || field.name === 'Test Description:') {
340
- let paragraphSkin = new JSONRichTextParagraph(
341
- field,
342
- styles,
343
- wi.Source,
344
- headingLvl + wi.level
345
- );
346
- paragraphs.push(paragraphSkin.getJSONRichTextParagraph());
347
- }
348
-
349
- //making sure ID is not printed
350
- //excluding Description and Test Description are handled in richText handler
351
- if (
352
- field.name !== 'ID' &&
353
- field.name !== 'Description: ' &&
354
- field.name !== 'Test Description:'
355
- ) {
356
- let paragraphSkin = new JSONParagraph(field, styles, wi.Source, headingLvl + wi.level);
357
- paragraphs.push(paragraphSkin.getJSONParagraph());
358
- }
359
- });
360
- });
361
- return paragraphs;
395
+ return this.generateParagraphsFromData(data, styles, headingLvl);
362
396
  case 'html':
363
397
  logger.info(`Generating html paragraphs!`);
364
398
  return [];
365
399
  default:
366
400
  throw new Error(`Invalid skin format ${this.skinFormat}`);
367
- } //switch
368
- } catch (error) {
401
+ }
402
+ } catch (error: any) {
369
403
  logger.error(`Error occurred in generateQueryBasedParagraphs: ${error.message}`);
370
404
  throw error;
371
405
  }
372
- } //generateParagraph
406
+ }
373
407
 
374
408
  generateTraceTable(
375
409
  data: any,
@@ -397,7 +431,8 @@ export default class Skins {
397
431
  undefined,
398
432
  undefined,
399
433
  false,
400
- groupedHeader
434
+ groupedHeader,
435
+ true
401
436
  );
402
437
  traceSkin.push(tableSkin.getJSONTable());
403
438
  } else if (errorMessage !== null) {
@@ -423,7 +458,8 @@ export default class Skins {
423
458
  styles: StyleOptions,
424
459
  headingLvl: number = 0,
425
460
  includeAttachments: boolean = true,
426
- isFlattened = false
461
+ isFlattened = false,
462
+ headingLevelOffset = 0
427
463
  ): any[] {
428
464
  logger.debug(`Generating testSkin as ${this.skinFormat}`);
429
465
  let aggregatedErrors: string[] = [];
@@ -450,7 +486,7 @@ export default class Skins {
450
486
  testSuite?.suiteSkinData?.fields && testSuite.suiteSkinData.fields.length > 1
451
487
  ? Number(testSuite.suiteSkinData.fields[1]?.value)
452
488
  : undefined;
453
-
489
+
454
490
  // Check if suite should be included (not flattened)
455
491
  if (testSuite.suiteSkinData) {
456
492
  let SuiteStyles = {
@@ -468,10 +504,10 @@ export default class Skins {
468
504
  SuiteStyles,
469
505
  suiteId || 0,
470
506
  testSuite.suiteSkinData.level
471
- ? headingLvl + testSuite.suiteSkinData.level
507
+ ? headingLvl + testSuite.suiteSkinData.level + headingLevelOffset
472
508
  : headingLvl
473
- ? headingLvl
474
- : 1
509
+ ? headingLvl + headingLevelOffset
510
+ : 1 + headingLevelOffset
475
511
  );
476
512
  testSkin.push(testSuiteParagraphSkin.getJSONParagraph());
477
513
  }
@@ -497,10 +533,10 @@ export default class Skins {
497
533
  TestCaseStyles,
498
534
  testcase.id || 0,
499
535
  testcase.testCaseHeaderSkinData.level
500
- ? headingLvl + testcase.testCaseHeaderSkinData.level
536
+ ? headingLvl + testcase.testCaseHeaderSkinData.level + headingLevelOffset
501
537
  : headingLvl
502
- ? headingLvl
503
- : 1
538
+ ? headingLvl + headingLevelOffset
539
+ : 1 + headingLevelOffset
504
540
  );
505
541
  testSkin.push(testCaseParagraphSkin.getJSONParagraph());
506
542
 
@@ -521,6 +557,23 @@ export default class Skins {
521
557
  let richTextSkin: any[] = testDescriptionParagraph.getJSONRichTextParagraph();
522
558
  testSkin = [...testSkin, ...richTextSkin];
523
559
 
560
+ const additionalTestCaseFields = Array.isArray(testcase.testCaseHeaderSkinData?.fields)
561
+ ? testcase.testCaseHeaderSkinData.fields.slice(3)
562
+ : [];
563
+ additionalTestCaseFields.forEach((field: any) => {
564
+ const name = String(field?.name || '').trim();
565
+ const value = String(field?.value ?? '').trim();
566
+ if (!value) return;
567
+ const combinedValue = name ? `${name}: ${value}` : value;
568
+ const paragraph = new JSONParagraph(
569
+ { name: '', value: combinedValue },
570
+ styles,
571
+ testcase.id || 0,
572
+ 0
573
+ );
574
+ testSkin.push(paragraph.getJSONParagraph());
575
+ });
576
+
524
577
  if (testcase.testCaseRequirements) {
525
578
  if (testcase.testCaseRequirements.length > 0) {
526
579
  let testDescriptionTitleParagraph = new JSONParagraph(
@@ -12,6 +12,7 @@ import JSONTableRow from './JSONTableRow';
12
12
  export default class JSONTable {
13
13
  tableTemplate: Table;
14
14
  tableStyles: StyleOptions;
15
+ private readonly enableVerticalMerge: boolean;
15
16
 
16
17
  constructor(
17
18
  data: WIQueryResults,
@@ -29,8 +30,10 @@ export default class JSONTable {
29
30
  shading?: Shading;
30
31
  leftShading?: Shading;
31
32
  rightShading?: Shading;
32
- }
33
+ },
34
+ enableVerticalMerge: boolean = false
33
35
  ) {
36
+ this.enableVerticalMerge = enableVerticalMerge;
34
37
  this.tableStyles = tableStyles;
35
38
  this.tableTemplate = {
36
39
  type: 'table',
@@ -106,8 +109,10 @@ export default class JSONTable {
106
109
  rows.push(row.getRow());
107
110
  });
108
111
 
109
- // Apply vertical merging for grouped source cells (first row shows value; subsequent are empty)
110
- this.applyVerticalMerges(rows);
112
+ if (this.enableVerticalMerge) {
113
+ // Apply vertical merging for grouped source cells (first row shows value; subsequent are empty)
114
+ this.applyVerticalMerges(rows);
115
+ }
111
116
 
112
117
  return rows;
113
118
  } //generateJsonRows
@@ -179,6 +184,17 @@ export default class JSONTable {
179
184
 
180
185
  const getCellText = (cell: any): string => {
181
186
  try {
187
+ const htmlRaw = String(cell?.Html?.Html || '').trim();
188
+ if (htmlRaw) {
189
+ const htmlText = htmlRaw
190
+ .replace(/<[^>]*>/g, ' ')
191
+ .replace(/&nbsp;/gi, ' ')
192
+ .replace(/\s+/g, ' ')
193
+ .trim();
194
+ if (htmlText) {
195
+ return htmlText;
196
+ }
197
+ }
182
198
  const runs = cell?.Paragraphs?.[0]?.Runs || [];
183
199
  return runs.map((r: any) => (r?.text ?? '')).join('').trim();
184
200
  } catch {
@@ -0,0 +1,59 @@
1
+ import JSONFile from '../models/json/file/JSONFile';
2
+ import logger from '../services/logger';
3
+
4
+ jest.mock('../services/logger', () => ({
5
+ __esModule: true,
6
+ default: {
7
+ debug: jest.fn(),
8
+ info: jest.fn(),
9
+ warn: jest.fn(),
10
+ error: jest.fn(),
11
+ },
12
+ }));
13
+
14
+ const getMockLogger = () =>
15
+ logger as unknown as {
16
+ debug: jest.Mock;
17
+ info: jest.Mock;
18
+ warn: jest.Mock;
19
+ error: jest.Mock;
20
+ };
21
+
22
+ describe('JSONFile', () => {
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ test('removeFileExtension returns original name when there is no dot', () => {
28
+ const data: any = {
29
+ attachmentLink: 'http://server/file',
30
+ attachmentFileName: 'file',
31
+ attachmentType: 'asEmbedded',
32
+ includeAttachmentContent: true,
33
+ };
34
+
35
+ const jsonFile = new JSONFile(data);
36
+ const attachment = jsonFile.getFileAttachment as any;
37
+
38
+ expect(attachment.type).toBe('File');
39
+ expect(attachment.name).toBe('file');
40
+ });
41
+
42
+ test('isPictureUri handles invalid uri and logs error', () => {
43
+ const data: any = {
44
+ attachmentLink: undefined,
45
+ attachmentFileName: 'file.docx',
46
+ attachmentType: 'asEmbedded',
47
+ includeAttachmentContent: true,
48
+ };
49
+
50
+ const jsonFile = new JSONFile(data);
51
+ const attachment = jsonFile.getFileAttachment as any;
52
+
53
+ expect(attachment.type).toBe('File');
54
+ expect(attachment.name).toBe('file');
55
+ expect(getMockLogger().error).toHaveBeenCalledWith(
56
+ expect.stringContaining('error parsing file extension for')
57
+ );
58
+ });
59
+ });
@@ -0,0 +1,118 @@
1
+ import JSONRun from '../models/json/JSONRun';
2
+ import { defaultJsonRun } from '../models/json/default';
3
+ import logger from '../services/logger';
4
+
5
+ jest.mock('../services/logger', () => ({
6
+ __esModule: true,
7
+ default: {
8
+ debug: jest.fn(),
9
+ info: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ silly: jest.fn(),
13
+ },
14
+ }));
15
+
16
+ const getMockLogger = () =>
17
+ logger as unknown as {
18
+ debug: jest.Mock;
19
+ info: jest.Mock;
20
+ warn: jest.Mock;
21
+ error: jest.Mock;
22
+ silly: jest.Mock;
23
+ };
24
+
25
+ const baseStyles = {
26
+ isBold: true,
27
+ IsItalic: false,
28
+ IsUnderline: false,
29
+ Size: 12,
30
+ Uri: 'http://example.com',
31
+ Font: 'Arial',
32
+ InsertLineBreak: false,
33
+ InsertSpace: true,
34
+ InsertPageBreak: false as boolean | undefined,
35
+ };
36
+
37
+ describe('JSONRun', () => {
38
+ beforeEach(() => {
39
+ jest.clearAllMocks();
40
+ });
41
+
42
+ test('splits multi-line text into separate runs with correct line breaks', () => {
43
+ const value = 'line1\nline2\nline3';
44
+
45
+ const jsonRun = new JSONRun(value, { ...baseStyles });
46
+ const runs = jsonRun.getRun();
47
+
48
+ expect(runs).toHaveLength(3);
49
+
50
+ expect(runs[0].InsertLineBreak).toBe(true);
51
+ expect(runs[1].InsertLineBreak).toBe(true);
52
+ expect(runs[2].InsertLineBreak).toBe(baseStyles.InsertLineBreak);
53
+
54
+ runs.forEach((r) => {
55
+ expect(r.Bold).toBe(baseStyles.isBold);
56
+ expect(r.Italic).toBe(baseStyles.IsItalic);
57
+ expect(r.Underline).toBe(baseStyles.IsUnderline);
58
+ expect(r.Size).toBe(baseStyles.Size);
59
+ expect(r.Font).toBe(baseStyles.Font);
60
+ expect(r.InsertSpace).toBe(baseStyles.InsertSpace);
61
+ });
62
+ });
63
+
64
+ test('uses striphtml replaceBr callback to convert <br> to newline', () => {
65
+ const value = 'Hello<br>World';
66
+
67
+ const jsonRun = new JSONRun(value, { ...baseStyles });
68
+ const runs = jsonRun.getRun();
69
+
70
+ expect(runs).toHaveLength(1);
71
+ expect(runs[0].text).toContain('\n');
72
+ });
73
+
74
+ test('sets Uri to null when default run Uri is empty string in normal flow', () => {
75
+ const prevUri = defaultJsonRun.Uri;
76
+ defaultJsonRun.Uri = '' as any;
77
+
78
+ const jsonRun = new JSONRun('plain text', { ...baseStyles });
79
+ const runs = jsonRun.getRun();
80
+
81
+ runs.forEach((r) => {
82
+ expect(r.Uri).toBeNull();
83
+ });
84
+
85
+ defaultJsonRun.Uri = prevUri;
86
+ });
87
+
88
+ test('does not strip HTML when fieldtype is SuiteHeaderParagraphTitle', () => {
89
+ const value = '<b>Bold Title</b>';
90
+
91
+ const jsonRun = new JSONRun(value, { ...baseStyles }, 'SuiteHeaderParagraphTitle');
92
+ const runs = jsonRun.getRun();
93
+
94
+ expect(runs).toHaveLength(1);
95
+ expect(runs[0].text).toContain('<b>Bold Title</b>');
96
+ });
97
+
98
+ test('falls back to default run and logs when value cannot be split', () => {
99
+ const badValue: any = null;
100
+
101
+ const prevUri = defaultJsonRun.Uri;
102
+ defaultJsonRun.Uri = '' as any;
103
+
104
+ const jsonRun = new JSONRun(badValue, { ...baseStyles });
105
+ const runs = jsonRun.getRun();
106
+
107
+ expect(getMockLogger().silly).toHaveBeenCalled();
108
+ expect(runs).toHaveLength(1);
109
+ expect(runs[0].text).toBe('');
110
+ expect(runs[0].Bold).toBe(baseStyles.isBold);
111
+ expect(runs[0].Italic).toBe(baseStyles.IsItalic);
112
+ expect(runs[0].Underline).toBe(baseStyles.IsUnderline);
113
+ expect(runs[0].Size).toBe(baseStyles.Size);
114
+ expect(runs[0].Uri).toBeNull();
115
+
116
+ defaultJsonRun.Uri = prevUri;
117
+ });
118
+ });
@@ -0,0 +1,126 @@
1
+ import JSONTable from '../models/json/table/JSONTable';
2
+
3
+ const baseStyles = {
4
+ isBold: false,
5
+ IsItalic: false,
6
+ IsUnderline: false,
7
+ Size: 10,
8
+ Uri: null as string | null,
9
+ Font: 'Arial',
10
+ InsertLineBreak: false,
11
+ InsertSpace: false,
12
+ };
13
+
14
+ const headerStyles = {
15
+ ...baseStyles,
16
+ isBold: true,
17
+ };
18
+
19
+ const buildData = (secondColValue: string) => [
20
+ {
21
+ url: '',
22
+ Source: 1,
23
+ level: 1,
24
+ fields: [
25
+ { name: '#', value: 1, width: '8%' },
26
+ { name: 'Description', value: 'No description' },
27
+ ],
28
+ },
29
+ {
30
+ url: '',
31
+ Source: 2,
32
+ level: 1,
33
+ fields: [
34
+ { name: '#', value: 2, width: '8%' },
35
+ { name: 'Description', value: secondColValue },
36
+ ],
37
+ },
38
+ ];
39
+
40
+ describe('JSONTable vertical merge behavior', () => {
41
+ test('uses explicit grouped-header column spans when provided', () => {
42
+ const table = new JSONTable(
43
+ [
44
+ {
45
+ url: '',
46
+ Source: 1,
47
+ level: 1,
48
+ fields: [
49
+ { name: 'Req ID', value: 1 },
50
+ { name: 'Title', value: 'Req' },
51
+ { name: 'Customer ID', value: 'C1' },
52
+ { name: 'Test Case ID', value: 11 },
53
+ { name: 'Title', value: 'TC' },
54
+ ],
55
+ },
56
+ ] as any,
57
+ headerStyles as any,
58
+ baseStyles as any,
59
+ 0,
60
+ false,
61
+ false,
62
+ false,
63
+ {
64
+ leftLabel: 'Requirement',
65
+ rightLabel: 'Test Case',
66
+ leftColumns: 3,
67
+ rightColumns: 2,
68
+ },
69
+ false
70
+ ).getJSONTable() as any;
71
+
72
+ expect(table.Rows[0].Cells[0].gridSpan).toBe(3);
73
+ expect(table.Rows[0].Cells[1].gridSpan).toBe(2);
74
+ });
75
+
76
+ test('does not merge empty cells when vertical merge is disabled', () => {
77
+ const table = new JSONTable(
78
+ buildData('') as any,
79
+ headerStyles as any,
80
+ baseStyles as any,
81
+ 0,
82
+ false,
83
+ false,
84
+ false,
85
+ undefined,
86
+ false
87
+ ).getJSONTable() as any;
88
+
89
+ expect(table.Rows[1].Cells[1].vMerge).toBeUndefined();
90
+ expect(table.Rows[2].Cells[1].vMerge).toBeUndefined();
91
+ });
92
+
93
+ test('merges empty cells when vertical merge is enabled', () => {
94
+ const table = new JSONTable(
95
+ buildData('') as any,
96
+ headerStyles as any,
97
+ baseStyles as any,
98
+ 0,
99
+ false,
100
+ false,
101
+ false,
102
+ undefined,
103
+ true
104
+ ).getJSONTable() as any;
105
+
106
+ expect(table.Rows[1].Cells[1].vMerge).toBe('restart');
107
+ expect(table.Rows[2].Cells[1].vMerge).toBe('continue');
108
+ });
109
+
110
+ test('does not treat HTML cell content as empty during merge detection', () => {
111
+ const table = new JSONTable(
112
+ buildData('<div>Some Description for suite</div>') as any,
113
+ headerStyles as any,
114
+ baseStyles as any,
115
+ 0,
116
+ false,
117
+ false,
118
+ false,
119
+ undefined,
120
+ true
121
+ ).getJSONTable() as any;
122
+
123
+ expect(table.Rows[2].Cells[1].vMerge).toBeUndefined();
124
+ expect(table.Rows[2].Cells[1].Html?.Html).toBe('<div>Some Description for suite</div>');
125
+ });
126
+ });