@elisra-devops/docgen-skins 0.23.0 → 0.25.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 (65) hide show
  1. package/.github/workflows/ci.yml +18 -9
  2. package/.github/workflows/release.yml +22 -6
  3. package/Readme.md +11 -0
  4. package/bin/index.d.ts +12 -1
  5. package/bin/index.js +201 -16
  6. package/bin/index.js.map +1 -1
  7. package/bin/models/json/JSONRun.js +2 -0
  8. package/bin/models/json/JSONRun.js.map +1 -1
  9. package/bin/models/json/file/JSONFile.js.map +1 -1
  10. package/bin/models/json/paragraph/JSONParagraph.js.map +1 -1
  11. package/bin/models/json/paragraph/JSONRichTextParagraph.js.map +1 -1
  12. package/bin/models/json/paragraph/JSONTestSuiteHeaderParagraph.js +3 -0
  13. package/bin/models/json/paragraph/JSONTestSuiteHeaderParagraph.js.map +1 -1
  14. package/bin/models/json/table/JSONTable.d.ts +2 -1
  15. package/bin/models/json/table/JSONTable.js +20 -6
  16. package/bin/models/json/table/JSONTable.js.map +1 -1
  17. package/bin/models/json/table/JSONTableCell.js +2 -0
  18. package/bin/models/json/table/JSONTableCell.js.map +1 -1
  19. package/bin/models/json/table/JSONTableRow.js.map +1 -1
  20. package/bin/models/json/wordJsonModels.d.ts +5 -0
  21. package/bin/services/logger.js +17 -7
  22. package/bin/services/logger.js.map +1 -1
  23. package/bin/test/JSONFile.test.js +48 -0
  24. package/bin/test/JSONFile.test.js.map +1 -0
  25. package/bin/test/JSONRun.test.d.ts +1 -0
  26. package/bin/test/JSONRun.test.js +93 -0
  27. package/bin/test/JSONRun.test.js.map +1 -0
  28. package/bin/test/JSONTable.test.d.ts +1 -0
  29. package/bin/test/JSONTable.test.js +79 -0
  30. package/bin/test/JSONTable.test.js.map +1 -0
  31. package/bin/test/JSONTableCell.test.d.ts +1 -0
  32. package/bin/test/JSONTableCell.test.js +154 -0
  33. package/bin/test/JSONTableCell.test.js.map +1 -0
  34. package/bin/test/JSONTableRow.test.d.ts +1 -0
  35. package/bin/test/JSONTableRow.test.js +40 -0
  36. package/bin/test/JSONTableRow.test.js.map +1 -0
  37. package/bin/test/JSONTestSuiteHeaderParagraph.test.d.ts +1 -0
  38. package/bin/test/JSONTestSuiteHeaderParagraph.test.js +57 -0
  39. package/bin/test/JSONTestSuiteHeaderParagraph.test.js.map +1 -0
  40. package/bin/test/SkinsEdgeCases.test.d.ts +1 -0
  41. package/bin/test/SkinsEdgeCases.test.js +399 -0
  42. package/bin/test/SkinsEdgeCases.test.js.map +1 -0
  43. package/bin/test/integration/skins.integration.test.d.ts +1 -0
  44. package/bin/test/integration/skins.integration.test.js +533 -0
  45. package/bin/test/integration/skins.integration.test.js.map +1 -0
  46. package/package.json +5 -5
  47. package/src/index.ts +247 -10
  48. package/src/models/json/JSONRun.ts +2 -0
  49. package/src/models/json/paragraph/JSONTestSuiteHeaderParagraph.ts +4 -0
  50. package/src/models/json/table/JSONTable.ts +19 -3
  51. package/src/models/json/table/JSONTableCell.ts +1 -0
  52. package/src/models/json/wordJsonModels.ts +5 -0
  53. package/src/test/JSONFile.test.ts +59 -0
  54. package/src/test/JSONRun.test.ts +118 -0
  55. package/src/test/JSONTable.test.ts +126 -0
  56. package/src/test/JSONTableCell.test.ts +181 -0
  57. package/src/test/JSONTableRow.test.ts +42 -0
  58. package/src/test/JSONTestSuiteHeaderParagraph.test.ts +64 -0
  59. package/src/test/SkinsEdgeCases.test.ts +530 -0
  60. package/src/test/integration/skins.integration.test.ts +660 -0
  61. package/tsconfig.json +2 -2
  62. package/bin/test/skins.test.js +0 -126
  63. package/bin/test/skins.test.js.map +0 -1
  64. package/src/test/skins.test.ts +0 -189
  65. /package/bin/test/{skins.test.d.ts → JSONFile.test.d.ts} +0 -0
package/src/index.ts CHANGED
@@ -22,9 +22,13 @@ export default class Skins {
22
22
  SKIN_TYPE_PARAGRAPH = 'paragraph';
23
23
  SKIN_TYPE_COVER_PAGE = 'cover-page';
24
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';
25
28
  SKIN_TYPE_SYSTEM_OVERVIEW = 'system-overview';
26
29
  SKIN_TYPE_INSTALLATION = 'installation';
27
30
  SKIN_TYPE_TEST_REPORTER = 'test-reporter';
31
+ SKIN_TYPE_TIME_MACHINE = 'time-machine-report';
28
32
 
29
33
  documentSkin: DocumentSkin = {
30
34
  templatePath: '',
@@ -38,6 +42,16 @@ export default class Skins {
38
42
  this.skinFormat = skinFormat;
39
43
  }
40
44
 
45
+ private resolveSkinTypeAlias(skinType: string) {
46
+ if (skinType === this.SKIN_TYPE_TEST_STD || skinType === this.SKIN_TYPE_TEST_STP) {
47
+ return this.SKIN_TYPE_TEST_PLAN;
48
+ }
49
+ if (skinType === this.SKIN_TYPE_TEST_STR) {
50
+ return this.SKIN_TYPE_TABLE_STR;
51
+ }
52
+ return skinType;
53
+ }
54
+
41
55
  async addNewContentToDocumentSkin(
42
56
  contentControlTitle: string,
43
57
  skinType: string,
@@ -51,8 +65,10 @@ export default class Skins {
51
65
  ): Promise<any[]> {
52
66
  try {
53
67
  let populatedSkin: any[] = [];
68
+ const isStpTestSkin = skinType === this.SKIN_TYPE_TEST_STP;
69
+ const normalizedSkinType = this.resolveSkinTypeAlias(skinType);
54
70
 
55
- switch (skinType) {
71
+ switch (normalizedSkinType) {
56
72
  case this.SKIN_TYPE_TABLE:
57
73
  populatedSkin = this.generateQueryBasedTable(data, headerStyles, styles, headingLvl);
58
74
 
@@ -84,7 +100,8 @@ export default class Skins {
84
100
  styles,
85
101
  headingLvl,
86
102
  includeAttachments,
87
- isFlattened
103
+ isFlattened,
104
+ isStpTestSkin ? 1 : 0
88
105
  );
89
106
  break;
90
107
  case this.SKIN_TYPE_SYSTEM_OVERVIEW:
@@ -96,6 +113,9 @@ export default class Skins {
96
113
  case this.SKIN_TYPE_TEST_REPORTER:
97
114
  populatedSkin = this.generateTestReporter(data, contentControlTitle);
98
115
  break;
116
+ case this.SKIN_TYPE_TIME_MACHINE:
117
+ populatedSkin = this.generateTimeMachineReport(data, headerStyles, styles, headingLvl);
118
+ break;
99
119
  default:
100
120
  throw new Error(`Unknown skinType : ${skinType} - not appended to document skin`);
101
121
  }
@@ -415,7 +435,8 @@ export default class Skins {
415
435
  undefined,
416
436
  undefined,
417
437
  false,
418
- groupedHeader
438
+ groupedHeader,
439
+ true
419
440
  );
420
441
  traceSkin.push(tableSkin.getJSONTable());
421
442
  } else if (errorMessage !== null) {
@@ -441,7 +462,8 @@ export default class Skins {
441
462
  styles: StyleOptions,
442
463
  headingLvl: number = 0,
443
464
  includeAttachments: boolean = true,
444
- isFlattened = false
465
+ isFlattened = false,
466
+ headingLevelOffset = 0
445
467
  ): any[] {
446
468
  logger.debug(`Generating testSkin as ${this.skinFormat}`);
447
469
  let aggregatedErrors: string[] = [];
@@ -486,10 +508,10 @@ export default class Skins {
486
508
  SuiteStyles,
487
509
  suiteId || 0,
488
510
  testSuite.suiteSkinData.level
489
- ? headingLvl + testSuite.suiteSkinData.level
511
+ ? headingLvl + testSuite.suiteSkinData.level + headingLevelOffset
490
512
  : headingLvl
491
- ? headingLvl
492
- : 1
513
+ ? headingLvl + headingLevelOffset
514
+ : 1 + headingLevelOffset
493
515
  );
494
516
  testSkin.push(testSuiteParagraphSkin.getJSONParagraph());
495
517
  }
@@ -515,10 +537,10 @@ export default class Skins {
515
537
  TestCaseStyles,
516
538
  testcase.id || 0,
517
539
  testcase.testCaseHeaderSkinData.level
518
- ? headingLvl + testcase.testCaseHeaderSkinData.level
540
+ ? headingLvl + testcase.testCaseHeaderSkinData.level + headingLevelOffset
519
541
  : headingLvl
520
- ? headingLvl
521
- : 1
542
+ ? headingLvl + headingLevelOffset
543
+ : 1 + headingLevelOffset
522
544
  );
523
545
  testSkin.push(testCaseParagraphSkin.getJSONParagraph());
524
546
 
@@ -539,6 +561,23 @@ export default class Skins {
539
561
  let richTextSkin: any[] = testDescriptionParagraph.getJSONRichTextParagraph();
540
562
  testSkin = [...testSkin, ...richTextSkin];
541
563
 
564
+ const additionalTestCaseFields = Array.isArray(testcase.testCaseHeaderSkinData?.fields)
565
+ ? testcase.testCaseHeaderSkinData.fields.slice(3)
566
+ : [];
567
+ additionalTestCaseFields.forEach((field: any) => {
568
+ const name = String(field?.name || '').trim();
569
+ const value = String(field?.value ?? '').trim();
570
+ if (!value) return;
571
+ const combinedValue = name ? `${name}: ${value}` : value;
572
+ const paragraph = new JSONParagraph(
573
+ { name: '', value: combinedValue },
574
+ styles,
575
+ testcase.id || 0,
576
+ 0
577
+ );
578
+ testSkin.push(paragraph.getJSONParagraph());
579
+ });
580
+
542
581
  if (testcase.testCaseRequirements) {
543
582
  if (testcase.testCaseRequirements.length > 0) {
544
583
  let testDescriptionTitleParagraph = new JSONParagraph(
@@ -759,6 +798,204 @@ export default class Skins {
759
798
  }
760
799
  }
761
800
 
801
+ private formatTimeMachineDate(value: any): string {
802
+ if (!value) return '';
803
+ const parsed = new Date(value);
804
+ if (Number.isNaN(parsed.getTime())) return String(value);
805
+ const month = parsed.toLocaleString('en-US', { month: 'long' });
806
+ const day = String(parsed.getDate()).padStart(2, '0');
807
+ const year = parsed.getFullYear();
808
+ const hours = String(parsed.getHours()).padStart(2, '0');
809
+ const minutes = String(parsed.getMinutes()).padStart(2, '0');
810
+ return `${month} ${day}, ${year} ${hours}:${minutes}`;
811
+ }
812
+
813
+ private asText(value: any): string {
814
+ if (value === null || value === undefined) return '';
815
+ if (typeof value === 'string') return value;
816
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
817
+ if (typeof value === 'object') {
818
+ try {
819
+ return JSON.stringify(value);
820
+ } catch {
821
+ return String(value);
822
+ }
823
+ }
824
+ return String(value);
825
+ }
826
+
827
+ private createParagraphWordObject(value: string, style: StyleOptions, headingLevel = 0) {
828
+ const paragraph = new JSONParagraph({ name: 'Title', value }, { ...style }, 0, headingLevel);
829
+ return paragraph.getJSONParagraph();
830
+ }
831
+
832
+ private createTextParagraphWordObject(value: string, style: StyleOptions) {
833
+ const paragraph = new JSONParagraph({ name: '', value }, { ...style }, 0, 0);
834
+ return paragraph.getJSONParagraph();
835
+ }
836
+
837
+ private createTableWordObject(rows: any[], headerStyles: StyleOptions, styles: StyleOptions) {
838
+ if (!Array.isArray(rows) || rows.length === 0) return null;
839
+ const table = new JSONTable(rows as any, { ...headerStyles }, { ...styles }, 0);
840
+ return table.getJSONTable();
841
+ }
842
+
843
+ generateTimeMachineReport(
844
+ data: any,
845
+ headerStyles: StyleOptions,
846
+ styles: StyleOptions,
847
+ headingLvl: number = 1
848
+ ): any[] {
849
+ const reportData = data?.compareResult ? data.compareResult : data || {};
850
+ const queryName = this.asText(data?.queryName || reportData?.queryName || '');
851
+ const teamProjectName = this.asText(data?.teamProjectName || reportData?.teamProjectName || '');
852
+ const baselineAsOf = reportData?.baseline?.asOf || '';
853
+ const compareToAsOf = reportData?.compareTo?.asOf || '';
854
+ const baselineTotal = Number(reportData?.baseline?.total || 0);
855
+ const compareToTotal = Number(reportData?.compareTo?.total || 0);
856
+ const summary = reportData?.summary || {};
857
+ const updatedCount = Number(summary?.updatedCount || 0);
858
+ const rows = Array.isArray(reportData?.rows) ? reportData.rows : [];
859
+ const changedRows = rows.filter((row) => String(row?.compareStatus || '').toLowerCase() === 'changed');
860
+
861
+ const titleStyles: StyleOptions = {
862
+ ...styles,
863
+ isBold: true,
864
+ InsertLineBreak: false,
865
+ InsertSpace: false,
866
+ };
867
+ const normalStyles: StyleOptions = {
868
+ ...styles,
869
+ isBold: false,
870
+ InsertLineBreak: false,
871
+ InsertSpace: false,
872
+ };
873
+ const tableHeaderStyles: StyleOptions = {
874
+ ...headerStyles,
875
+ isBold: true,
876
+ InsertLineBreak: false,
877
+ InsertSpace: false,
878
+ };
879
+
880
+ const reportWordObjects: any[] = [];
881
+ reportWordObjects.push(this.createParagraphWordObject('Difference', titleStyles, Math.max(1, headingLvl)));
882
+ reportWordObjects.push(
883
+ this.createTextParagraphWordObject(`Project: ${teamProjectName || 'N/A'}`, normalStyles),
884
+ );
885
+ if (queryName) {
886
+ reportWordObjects.push(this.createTextParagraphWordObject(`Query: ${queryName}`, normalStyles));
887
+ }
888
+
889
+ const compareSummaryTableRows = [
890
+ {
891
+ url: '',
892
+ fields: [
893
+ { name: 'Point', value: 'Baseline', width: '20%' },
894
+ { name: 'Time', value: this.formatTimeMachineDate(baselineAsOf), width: '50%' },
895
+ { name: '# Sum of Work-Items', value: String(baselineTotal), width: '30%' },
896
+ ],
897
+ Source: 0,
898
+ level: 0,
899
+ },
900
+ {
901
+ url: '',
902
+ fields: [
903
+ { name: 'Point', value: 'Compare to', width: '20%' },
904
+ { name: 'Time', value: this.formatTimeMachineDate(compareToAsOf), width: '50%' },
905
+ { name: '# Sum of Work-Items', value: String(compareToTotal), width: '30%' },
906
+ ],
907
+ Source: 0,
908
+ level: 0,
909
+ },
910
+ ];
911
+ const compareSummaryTable = this.createTableWordObject(compareSummaryTableRows, tableHeaderStyles, normalStyles);
912
+ if (compareSummaryTable) {
913
+ reportWordObjects.push(compareSummaryTable);
914
+ }
915
+
916
+ reportWordObjects.push(this.createParagraphWordObject('Summarize', titleStyles, Math.max(1, headingLvl + 2)));
917
+ reportWordObjects.push(
918
+ this.createTextParagraphWordObject(`# Sum of Work-Items updated: ${updatedCount}`, normalStyles),
919
+ );
920
+
921
+ if (rows.length > 0) {
922
+ const compareRows = rows.map((row) => ({
923
+ url: row?.workItemUrl || '',
924
+ fields: [
925
+ { name: 'ID', value: this.asText(row?.id), url: row?.workItemUrl || '', width: '9%' },
926
+ { name: 'Work Item Type', value: this.asText(row?.workItemType), width: '14%' },
927
+ { name: 'Title', value: this.asText(row?.title), width: '34%' },
928
+ { name: 'Id Revision of Baseline', value: this.asText(row?.baselineRevisionId), width: '14%' },
929
+ { name: 'Id Revision of CompareTo', value: this.asText(row?.compareToRevisionId), width: '14%' },
930
+ { name: 'Compare Status', value: this.asText(row?.compareStatus), width: '15%' },
931
+ ],
932
+ Source: Number(row?.id || 0),
933
+ level: 0,
934
+ }));
935
+ const compareTable = this.createTableWordObject(compareRows, tableHeaderStyles, normalStyles);
936
+ if (compareTable) {
937
+ reportWordObjects.push(compareTable);
938
+ }
939
+ } else {
940
+ reportWordObjects.push(this.createTextParagraphWordObject('No work-items were returned.', normalStyles));
941
+ }
942
+
943
+ reportWordObjects.push(
944
+ this.createParagraphWordObject('2. Work Item Differences', titleStyles, Math.max(1, headingLvl + 2)),
945
+ );
946
+ if (changedRows.length === 0) {
947
+ reportWordObjects.push(
948
+ this.createTextParagraphWordObject(
949
+ 'No work-items were marked as Changed between the selected date-times.',
950
+ normalStyles,
951
+ ),
952
+ );
953
+ return reportWordObjects;
954
+ }
955
+
956
+ changedRows.forEach((row) => {
957
+ const baselineRevision = this.asText(row?.baselineRevisionId || '');
958
+ const compareRevision = this.asText(row?.compareToRevisionId || '');
959
+ const differences = Array.isArray(row?.differences) ? row.differences : [];
960
+ const itemTitle = `${this.asText(row?.workItemType)} ${this.asText(row?.id)} Changed between version ${baselineRevision} and ${compareRevision}`;
961
+ reportWordObjects.push(
962
+ this.createParagraphWordObject(itemTitle.trim(), titleStyles, Math.max(1, headingLvl + 3)),
963
+ );
964
+ if (differences.length === 0) {
965
+ reportWordObjects.push(this.createTextParagraphWordObject('No field differences found.', normalStyles));
966
+ return;
967
+ }
968
+
969
+ differences.forEach((diff) => {
970
+ reportWordObjects.push(
971
+ this.createTextParagraphWordObject(`${this.asText(diff?.field)}:`, {
972
+ ...normalStyles,
973
+ isBold: true,
974
+ }),
975
+ );
976
+ const baselineValue = [baselineRevision, this.asText(diff?.baseline)].filter(Boolean).join(' ');
977
+ const compareValue = [compareRevision, this.asText(diff?.compareTo)].filter(Boolean).join(' ');
978
+ const diffTableRows = [
979
+ {
980
+ url: '',
981
+ fields: [
982
+ { name: 'Baseline', value: baselineValue, width: '50%' },
983
+ { name: 'Compare to', value: compareValue, width: '50%' },
984
+ ],
985
+ Source: Number(row?.id || 0),
986
+ level: 0,
987
+ },
988
+ ];
989
+ const diffTable = this.createTableWordObject(diffTableRows, tableHeaderStyles, normalStyles);
990
+ if (diffTable) {
991
+ reportWordObjects.push(diffTable);
992
+ }
993
+ });
994
+ });
995
+
996
+ return reportWordObjects;
997
+ }
998
+
762
999
  getDocumentSkin(): DocumentSkin {
763
1000
  return this.documentSkin;
764
1001
  } //getDocumentSkin
@@ -49,6 +49,7 @@ export default class JSONRun {
49
49
  run.InsertLineBreak = style.InsertLineBreak;
50
50
  }
51
51
  run.InsertSpace = style.InsertSpace;
52
+ run.SectionRefId = style.SectionRefId || null;
52
53
  runs.push(JSON.parse(JSON.stringify(run)));
53
54
  });
54
55
  } catch (e) {
@@ -67,6 +68,7 @@ export default class JSONRun {
67
68
  runs[0].Font = style.Font;
68
69
  runs[0].InsertLineBreak = style.InsertLineBreak;
69
70
  runs[0].InsertSpace = style.InsertSpace;
71
+ runs[0].SectionRefId = style.SectionRefId || null;
70
72
  }
71
73
 
72
74
  return runs;
@@ -30,6 +30,10 @@ export default class JSONTestSuiteHeaderParagraph {
30
30
  } else {
31
31
  this.paragraphTemplate.headingLevel = 0;
32
32
  }
33
+
34
+ if (this.wiId) {
35
+ this.paragraphTemplate.bookmarkName = `_WI${this.wiId}`;
36
+ }
33
37
  } //constructor
34
38
 
35
39
  generateJsonParagraphHeaderRuns(fields, paragraphStyles) {
@@ -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 {
@@ -87,6 +87,7 @@ export default class JSONTableCell {
87
87
  HtmlData = data.value || '';
88
88
  } else {
89
89
  styles.Uri = data.relativeUrl ? data.relativeUrl : data.url ? data.url : null;
90
+ styles.SectionRefId = data.sectionRefId ?? null;
90
91
  let text = data.value || '';
91
92
  let jsonRun = new JSONRun(`${text}`, styles);
92
93
  runs = [...runs, ...jsonRun.getRun()];
@@ -8,6 +8,7 @@ export interface StyleOptions {
8
8
  InsertLineBreak: boolean;
9
9
  InsertSpace: boolean;
10
10
  InsertPageBreak?: boolean;
11
+ SectionRefId?: string;
11
12
  }
12
13
  export interface DocumentSkin {
13
14
  templatePath: string;
@@ -51,6 +52,7 @@ export interface Paragraph {
51
52
  type: string;
52
53
  headingLevel?: number;
53
54
  runs: Run[];
55
+ bookmarkName?: string;
54
56
  }
55
57
  export interface Attachment {
56
58
  type: string;
@@ -73,6 +75,7 @@ export interface Run {
73
75
  InsertSpace: boolean;
74
76
  InsertPageBreak?: boolean;
75
77
  src?: string; //for image
78
+ SectionRefId?: string;
76
79
  }
77
80
  export interface WorkItemData {}
78
81
 
@@ -149,6 +152,7 @@ export interface WIProperty {
149
152
  includeAttachmentContent?: boolean;
150
153
  relativeAttachmentLink?: string;
151
154
  richText?: any[];
155
+ sectionRefId?: string;
152
156
  }
153
157
  export interface MultipeValuesWIProperty {
154
158
  name: string;
@@ -162,6 +166,7 @@ export interface MultipeValuesWIProperty {
162
166
  includeAttachmentContent?: boolean;
163
167
  relativeAttachmentLink?: string;
164
168
  richText?: any[];
169
+ sectionRefId?: string;
165
170
  }
166
171
  export interface WIColumns {
167
172
  name: string;
@@ -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
+ });