@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.
- package/.github/workflows/ci.yml +18 -9
- package/.github/workflows/release.yml +22 -6
- package/Readme.md +11 -0
- package/bin/index.d.ts +12 -1
- package/bin/index.js +201 -16
- package/bin/index.js.map +1 -1
- package/bin/models/json/JSONRun.js +2 -0
- package/bin/models/json/JSONRun.js.map +1 -1
- package/bin/models/json/file/JSONFile.js.map +1 -1
- package/bin/models/json/paragraph/JSONParagraph.js.map +1 -1
- package/bin/models/json/paragraph/JSONRichTextParagraph.js.map +1 -1
- package/bin/models/json/paragraph/JSONTestSuiteHeaderParagraph.js +3 -0
- package/bin/models/json/paragraph/JSONTestSuiteHeaderParagraph.js.map +1 -1
- package/bin/models/json/table/JSONTable.d.ts +2 -1
- package/bin/models/json/table/JSONTable.js +20 -6
- package/bin/models/json/table/JSONTable.js.map +1 -1
- package/bin/models/json/table/JSONTableCell.js +2 -0
- package/bin/models/json/table/JSONTableCell.js.map +1 -1
- package/bin/models/json/table/JSONTableRow.js.map +1 -1
- package/bin/models/json/wordJsonModels.d.ts +5 -0
- package/bin/services/logger.js +17 -7
- package/bin/services/logger.js.map +1 -1
- package/bin/test/JSONFile.test.js +48 -0
- package/bin/test/JSONFile.test.js.map +1 -0
- package/bin/test/JSONRun.test.d.ts +1 -0
- package/bin/test/JSONRun.test.js +93 -0
- package/bin/test/JSONRun.test.js.map +1 -0
- package/bin/test/JSONTable.test.d.ts +1 -0
- package/bin/test/JSONTable.test.js +79 -0
- package/bin/test/JSONTable.test.js.map +1 -0
- package/bin/test/JSONTableCell.test.d.ts +1 -0
- package/bin/test/JSONTableCell.test.js +154 -0
- package/bin/test/JSONTableCell.test.js.map +1 -0
- package/bin/test/JSONTableRow.test.d.ts +1 -0
- package/bin/test/JSONTableRow.test.js +40 -0
- package/bin/test/JSONTableRow.test.js.map +1 -0
- package/bin/test/JSONTestSuiteHeaderParagraph.test.d.ts +1 -0
- package/bin/test/JSONTestSuiteHeaderParagraph.test.js +57 -0
- package/bin/test/JSONTestSuiteHeaderParagraph.test.js.map +1 -0
- package/bin/test/SkinsEdgeCases.test.d.ts +1 -0
- package/bin/test/SkinsEdgeCases.test.js +399 -0
- package/bin/test/SkinsEdgeCases.test.js.map +1 -0
- package/bin/test/integration/skins.integration.test.d.ts +1 -0
- package/bin/test/integration/skins.integration.test.js +533 -0
- package/bin/test/integration/skins.integration.test.js.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +247 -10
- package/src/models/json/JSONRun.ts +2 -0
- package/src/models/json/paragraph/JSONTestSuiteHeaderParagraph.ts +4 -0
- package/src/models/json/table/JSONTable.ts +19 -3
- package/src/models/json/table/JSONTableCell.ts +1 -0
- package/src/models/json/wordJsonModels.ts +5 -0
- package/src/test/JSONFile.test.ts +59 -0
- package/src/test/JSONRun.test.ts +118 -0
- package/src/test/JSONTable.test.ts +126 -0
- package/src/test/JSONTableCell.test.ts +181 -0
- package/src/test/JSONTableRow.test.ts +42 -0
- package/src/test/JSONTestSuiteHeaderParagraph.test.ts +64 -0
- package/src/test/SkinsEdgeCases.test.ts +530 -0
- package/src/test/integration/skins.integration.test.ts +660 -0
- package/tsconfig.json +2 -2
- package/bin/test/skins.test.js +0 -126
- package/bin/test/skins.test.js.map +0 -1
- package/src/test/skins.test.ts +0 -189
- /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 (
|
|
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
|
-
|
|
110
|
-
|
|
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(/ /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
|
+
});
|