@sun-asterisk/sungen 3.2.6 → 3.2.7
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/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +7 -46
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +28 -6
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +20 -20
- package/dist/dashboard/types.d.ts +18 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts +18 -0
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -0
- package/dist/exporters/api-catalog-loader.js +77 -0
- package/dist/exporters/api-catalog-loader.js.map +1 -0
- package/dist/exporters/json-exporter.d.ts +10 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +34 -2
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +14 -0
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +30 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +18 -17
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +25 -595
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts +196 -0
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -0
- package/dist/exporters/xlsx-report-builder.js +653 -0
- package/dist/exporters/xlsx-report-builder.js.map +1 -0
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +8 -42
- package/src/dashboard/snapshot-builder.ts +31 -7
- package/src/dashboard/templates/index.html +20 -20
- package/src/dashboard/types.ts +19 -0
- package/src/exporters/api-catalog-loader.ts +44 -0
- package/src/exporters/json-exporter.ts +46 -4
- package/src/exporters/playwright-report-parser.ts +27 -0
- package/src/exporters/xlsx-exporter.ts +34 -658
- package/src/exporters/xlsx-report-builder.ts +806 -0
- package/dist/cli/types.d.ts +0 -9
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/types.js +0 -7
- package/dist/cli/types.js.map +0 -1
- package/dist/generators/gherkin-parser/selector-extractor.d.ts +0 -37
- package/dist/generators/gherkin-parser/selector-extractor.d.ts.map +0 -1
- package/dist/generators/gherkin-parser/selector-extractor.js +0 -108
- package/dist/generators/gherkin-parser/selector-extractor.js.map +0 -1
- package/dist/generators/test-generator/types.d.ts +0 -25
- package/dist/generators/test-generator/types.d.ts.map +0 -1
- package/dist/generators/test-generator/types.js +0 -7
- package/dist/generators/test-generator/types.js.map +0 -1
- package/dist/generators/types.d.ts +0 -119
- package/dist/generators/types.d.ts.map +0 -1
- package/dist/generators/types.js +0 -48
- package/dist/generators/types.js.map +0 -1
- package/dist/orchestrator/figma/node-path-collapser.d.ts +0 -16
- package/dist/orchestrator/figma/node-path-collapser.d.ts.map +0 -1
- package/dist/orchestrator/figma/node-path-collapser.js +0 -37
- package/dist/orchestrator/figma/node-path-collapser.js.map +0 -1
- package/src/cli/types.ts +0 -9
- package/src/generators/gherkin-parser/selector-extractor.ts +0 -142
- package/src/generators/test-generator/types.ts +0 -26
- package/src/generators/types.ts +0 -227
- package/src/orchestrator/figma/node-path-collapser.ts +0 -38
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure XLSX workbook builder — the single source of truth for the BM-2-901-13
|
|
4
|
+
* delivery report rendering, shared by the CLI exporter
|
|
5
|
+
* (`xlsx-exporter.ts`, Node) and the dashboard's in-browser exporter
|
|
6
|
+
* (`dashboard/src/lib/export.ts`, bundled via vite alias).
|
|
7
|
+
*
|
|
8
|
+
* BROWSER-SAFE: this module may import ONLY `exceljs` and `./sun-logo`.
|
|
9
|
+
* No `fs`, no `path`, no `./package-info` (version lookup is fs-based) and no
|
|
10
|
+
* `./feature-parser` (it transitively pulls the fs-reading gherkin parser).
|
|
11
|
+
* Anything environment-specific is injected by the caller:
|
|
12
|
+
* - the sungen version string → `opts.sungenVersion` / `sungenVersion` param
|
|
13
|
+
* - the tag-extractor helpers used by `buildApiDetailRows` → `TagExtractors`
|
|
14
|
+
*
|
|
15
|
+
* Render the same rows/summary the CSV exporter produces into a styled .xlsx
|
|
16
|
+
* workbook. Keeps the BM-2-901-13 layout but adds:
|
|
17
|
+
* - bold header row with filled background + white font
|
|
18
|
+
* - borders on the data region
|
|
19
|
+
* - text wrapping in Steps / Expected / Pre-condition / Test Data
|
|
20
|
+
* - sensible column widths
|
|
21
|
+
* - category-separator rows rendered as a filled bar spanning the table
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.API_DETAIL_HEADER_FONT = exports.API_DETAIL_HEADER_FILL = exports.OVERVIEW_HEADERS = exports.allBordersBlack = exports.LAVENDER = exports.WHITE = exports.BLACK = exports.ARIAL_FONT = exports.TIMES = exports.BORDER_COLOR = exports.CATEGORY_FILL = exports.META_FILL = exports.HEADER_FONT = exports.HEADER_FILL = exports.COL_COUNT = void 0;
|
|
28
|
+
exports.applyBorder = applyBorder;
|
|
29
|
+
exports.renderReportHeaderBand = renderReportHeaderBand;
|
|
30
|
+
exports.addOverviewSheet = addOverviewSheet;
|
|
31
|
+
exports.addTestcaseSheet = addTestcaseSheet;
|
|
32
|
+
exports.buildApiDetailRows = buildApiDetailRows;
|
|
33
|
+
exports.addApiDetailSheet = addApiDetailSheet;
|
|
34
|
+
exports.buildSingleTestcaseWorkbook = buildSingleTestcaseWorkbook;
|
|
35
|
+
exports.buildTestcaseWorkbook = buildTestcaseWorkbook;
|
|
36
|
+
const exceljs_1 = __importDefault(require("exceljs"));
|
|
37
|
+
const sun_logo_1 = require("./sun-logo");
|
|
38
|
+
exports.COL_COUNT = 16;
|
|
39
|
+
exports.HEADER_FILL = 'FFD9D2E9'; // lavender — matches the summary-header band on row 6
|
|
40
|
+
exports.HEADER_FONT = 'FF000000'; // black text reads better on the light lavender
|
|
41
|
+
exports.META_FILL = 'FFDCE6F1'; // soft blue for meta rows
|
|
42
|
+
exports.CATEGORY_FILL = 'FFD9D2E9'; // same lavender for group dividers (Accessing / GUI / Function)
|
|
43
|
+
exports.BORDER_COLOR = 'FFBFBFBF';
|
|
44
|
+
// Shared masthead + table styling (BM-2-901-13 header band, summary band, Overview table).
|
|
45
|
+
exports.TIMES = 'Times New Roman';
|
|
46
|
+
exports.ARIAL_FONT = 'Arial';
|
|
47
|
+
exports.BLACK = { argb: 'FF000000' };
|
|
48
|
+
exports.WHITE = { argb: 'FFFFFFFF' };
|
|
49
|
+
exports.LAVENDER = { argb: 'FFD9D2E9' };
|
|
50
|
+
const thinBlack = { style: 'thin', color: exports.BLACK };
|
|
51
|
+
exports.allBordersBlack = { top: thinBlack, left: thinBlack, bottom: thinBlack, right: thinBlack };
|
|
52
|
+
function applyBorder(cell) {
|
|
53
|
+
cell.border = {
|
|
54
|
+
top: { style: 'thin', color: { argb: exports.BORDER_COLOR } },
|
|
55
|
+
left: { style: 'thin', color: { argb: exports.BORDER_COLOR } },
|
|
56
|
+
bottom: { style: 'thin', color: { argb: exports.BORDER_COLOR } },
|
|
57
|
+
right: { style: 'thin', color: { argb: exports.BORDER_COLOR } },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render the shared BM-2-901-13 masthead (rows 1-4) onto a worksheet:
|
|
62
|
+
* Sun* logo (A1:C3), TESTCASE title (D1:F3), No/Version/Issue-Date block
|
|
63
|
+
* (G1:H3), company line (A4:F4), ISO line (G4:H4). Every sheet in a delivery
|
|
64
|
+
* workbook — Overview, Auto, Manual — carries this same header band.
|
|
65
|
+
*/
|
|
66
|
+
function renderReportHeaderBand(wb, ws, titleLabel, sungenVersion) {
|
|
67
|
+
const issueDate = (() => {
|
|
68
|
+
const d = new Date();
|
|
69
|
+
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
|
70
|
+
})();
|
|
71
|
+
// Ensure rows 1..4 exist before merging.
|
|
72
|
+
for (let r = 1; r <= 4; r++)
|
|
73
|
+
ws.getRow(r);
|
|
74
|
+
ws.mergeCells('A1:C3');
|
|
75
|
+
ws.mergeCells('D1:F3');
|
|
76
|
+
ws.mergeCells('G1:H1');
|
|
77
|
+
ws.mergeCells('G2:H2');
|
|
78
|
+
ws.mergeCells('G3:H3');
|
|
79
|
+
ws.mergeCells('A4:F4');
|
|
80
|
+
ws.mergeCells('G4:H4');
|
|
81
|
+
// A1 (logo band, merged A1:C3) — border + embedded Sun* logo (base64 inline).
|
|
82
|
+
const a1 = ws.getCell('A1');
|
|
83
|
+
a1.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
84
|
+
a1.border = exports.allBordersBlack;
|
|
85
|
+
try {
|
|
86
|
+
// base64 form (not Buffer.from) keeps this module Buffer-free / browser-safe;
|
|
87
|
+
// ExcelJS decodes it to the identical PNG media bytes either way.
|
|
88
|
+
const imageId = wb.addImage({
|
|
89
|
+
base64: sun_logo_1.SUN_LOGO_PNG_BASE64,
|
|
90
|
+
extension: 'png',
|
|
91
|
+
});
|
|
92
|
+
// Centre a fixed-size 90×51 logo inside A1:C3 using absolute EMU offsets.
|
|
93
|
+
ws.addImage(imageId, {
|
|
94
|
+
tl: { nativeCol: 0, nativeColOff: 1237500, nativeRow: 0, nativeRowOff: 42862 },
|
|
95
|
+
ext: { width: 90, height: 51 },
|
|
96
|
+
editAs: 'oneCell',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch { /* logo is decorative — never block export */ }
|
|
100
|
+
// D1 (TESTCASE title)
|
|
101
|
+
const d1 = ws.getCell('D1');
|
|
102
|
+
d1.value = titleLabel;
|
|
103
|
+
d1.font = { bold: true, size: 18, name: exports.TIMES };
|
|
104
|
+
d1.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
105
|
+
d1.border = exports.allBordersBlack;
|
|
106
|
+
// G1 — No: BM-2-901-13
|
|
107
|
+
const g1 = ws.getCell('G1');
|
|
108
|
+
g1.value = 'No: BM-2-901-13';
|
|
109
|
+
g1.font = { size: 12, name: exports.TIMES };
|
|
110
|
+
g1.alignment = { vertical: 'middle' };
|
|
111
|
+
g1.border = { top: thinBlack, left: thinBlack, right: thinBlack };
|
|
112
|
+
// G2 — Version
|
|
113
|
+
const g2 = ws.getCell('G2');
|
|
114
|
+
g2.value = `Version: ${sungenVersion ?? 'unknown'}`;
|
|
115
|
+
g2.font = { size: 12, name: exports.TIMES };
|
|
116
|
+
g2.alignment = { vertical: 'middle' };
|
|
117
|
+
g2.border = { left: thinBlack, right: thinBlack };
|
|
118
|
+
// G3 — Issue Date
|
|
119
|
+
const g3 = ws.getCell('G3');
|
|
120
|
+
g3.value = `Issue Date: ${issueDate}`;
|
|
121
|
+
g3.font = { size: 12, name: exports.TIMES };
|
|
122
|
+
g3.alignment = { vertical: 'middle' };
|
|
123
|
+
g3.border = { left: thinBlack, right: thinBlack, bottom: thinBlack };
|
|
124
|
+
// A4 — SUN ASTERISK VIETNAM CO., LTD
|
|
125
|
+
const a4 = ws.getCell('A4');
|
|
126
|
+
a4.value = 'SUN ASTERISK VIETNAM CO., LTD';
|
|
127
|
+
a4.font = { bold: true, size: 10, name: exports.TIMES };
|
|
128
|
+
a4.alignment = { vertical: 'middle' };
|
|
129
|
+
a4.border = exports.allBordersBlack;
|
|
130
|
+
// G4 — ISO/IEC ...
|
|
131
|
+
const g4 = ws.getCell('G4');
|
|
132
|
+
g4.value = 'ISO/IEC 27001:2022 & ISO 9001:2015';
|
|
133
|
+
g4.font = { bold: true, size: 10, name: exports.TIMES };
|
|
134
|
+
g4.alignment = { horizontal: 'right', vertical: 'middle', wrapText: true };
|
|
135
|
+
g4.border = exports.allBordersBlack;
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Overview sheet — the first tab in every multi-sheet delivery workbook.
|
|
139
|
+
// Rolls up each testcase sheet's Passed/Failed/Pending/N/A/Remain/Total into
|
|
140
|
+
// one summary table (+ a grand-total row) under the shared masthead. Reads
|
|
141
|
+
// only each sheet's summary — never touches the detail sheets themselves.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
/** Column headers for the Overview roll-up table (cols A..H). */
|
|
144
|
+
exports.OVERVIEW_HEADERS = ['#', 'Sheet name', 'Passed', 'Failed', 'Pending', 'N/A', 'Remain', 'Total'];
|
|
145
|
+
/**
|
|
146
|
+
* Prepend an "Overview" worksheet summarizing every sheet in `sheets`.
|
|
147
|
+
*
|
|
148
|
+
* `Remain = Total − Passed − N/A` (i.e. Failed + Pending) so the number
|
|
149
|
+
* reconciles with the "Remaining" cell in each detail sheet's summary band.
|
|
150
|
+
*/
|
|
151
|
+
function addOverviewSheet(wb, sheets, sungenVersion) {
|
|
152
|
+
const ws = wb.addWorksheet('Overview');
|
|
153
|
+
// Table starts at column B row 7 (A is a thin left margin). Widths keep the
|
|
154
|
+
// A..H masthead readable AND the count columns tidy.
|
|
155
|
+
ws.columns = [
|
|
156
|
+
{ width: 4 }, // A — left margin
|
|
157
|
+
{ width: 6 }, // B — #
|
|
158
|
+
{ width: 26 }, // C — Sheet name
|
|
159
|
+
{ width: 13 }, // D — Passed
|
|
160
|
+
{ width: 13 }, // E — Failed
|
|
161
|
+
{ width: 13 }, // F — Pending
|
|
162
|
+
{ width: 13 }, // G — N/A
|
|
163
|
+
{ width: 13 }, // H — Remain
|
|
164
|
+
{ width: 13 }, // I — Total
|
|
165
|
+
];
|
|
166
|
+
const titleScreen = sheets[0]?.summary.screen ?? '';
|
|
167
|
+
renderReportHeaderBand(wb, ws, `${titleScreen.toUpperCase()} TESTCASE`, sungenVersion);
|
|
168
|
+
// Roll-up table: header on row 7, first column B (index 2).
|
|
169
|
+
const HEADER_ROW = 7;
|
|
170
|
+
const FIRST_COL = 2; // column B
|
|
171
|
+
const headerRow = ws.getRow(HEADER_ROW);
|
|
172
|
+
headerRow.height = 20;
|
|
173
|
+
exports.OVERVIEW_HEADERS.forEach((label, i) => {
|
|
174
|
+
const c = headerRow.getCell(FIRST_COL + i);
|
|
175
|
+
c.value = label;
|
|
176
|
+
c.font = { bold: true, size: 10, color: exports.BLACK, name: exports.ARIAL_FONT };
|
|
177
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: exports.LAVENDER };
|
|
178
|
+
c.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
179
|
+
c.border = exports.allBordersBlack;
|
|
180
|
+
});
|
|
181
|
+
const totals = { passed: 0, failed: 0, pending: 0, na: 0, remain: 0, total: 0 };
|
|
182
|
+
const writeDataRow = (rowIdx, values, isTotal) => {
|
|
183
|
+
const row = ws.getRow(rowIdx);
|
|
184
|
+
values.forEach((v, i) => {
|
|
185
|
+
const c = row.getCell(FIRST_COL + i);
|
|
186
|
+
c.value = v;
|
|
187
|
+
c.font = { bold: isTotal, size: 10, color: exports.BLACK, name: exports.ARIAL_FONT };
|
|
188
|
+
// The '#' + 'Sheet name' columns read left; the counts centre like the band.
|
|
189
|
+
c.alignment = { horizontal: i <= 1 ? 'left' : 'center', vertical: 'middle' };
|
|
190
|
+
c.border = exports.allBordersBlack;
|
|
191
|
+
if (isTotal)
|
|
192
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: exports.LAVENDER };
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
// Each detail sheet carries a LIVE summary band (row 7): C7 Total · D7 Passed · E7 Failed ·
|
|
196
|
+
// F7 Pending · G7 N/A · H7 Remaining, all COUNTA/COUNTIF over the data area. The Overview
|
|
197
|
+
// REFERENCES those cells (`='<sheet>'!D7`) instead of freezing a number at export time — so when
|
|
198
|
+
// QA adds/edits/removes a row in the Auto or Manual sheet, the roll-up recomputes automatically.
|
|
199
|
+
const qname = (name) => name.replace(/'/g, "''"); // Excel escapes ' by doubling
|
|
200
|
+
const ref = (name, cell, result) => ({ formula: `'${qname(name)}'!${cell}`, result });
|
|
201
|
+
let rowIdx = HEADER_ROW + 1;
|
|
202
|
+
const firstDataRow = rowIdx;
|
|
203
|
+
sheets.forEach((s, i) => {
|
|
204
|
+
const su = s.summary;
|
|
205
|
+
const remain = Math.max(0, su.total - su.passed - su.na);
|
|
206
|
+
writeDataRow(rowIdx, [
|
|
207
|
+
i + 1,
|
|
208
|
+
s.sheetName,
|
|
209
|
+
ref(s.sheetName, 'D7', su.passed),
|
|
210
|
+
ref(s.sheetName, 'E7', su.failed),
|
|
211
|
+
ref(s.sheetName, 'F7', su.pending),
|
|
212
|
+
ref(s.sheetName, 'G7', su.na),
|
|
213
|
+
ref(s.sheetName, 'H7', remain),
|
|
214
|
+
ref(s.sheetName, 'C7', su.total),
|
|
215
|
+
], false);
|
|
216
|
+
totals.passed += su.passed;
|
|
217
|
+
totals.failed += su.failed;
|
|
218
|
+
totals.pending += su.pending;
|
|
219
|
+
totals.na += su.na;
|
|
220
|
+
totals.remain += remain;
|
|
221
|
+
totals.total += su.total;
|
|
222
|
+
rowIdx += 1;
|
|
223
|
+
});
|
|
224
|
+
const lastDataRow = rowIdx - 1;
|
|
225
|
+
// Grand total = SUM of the per-sheet Overview cells (also live — recomputes with the references
|
|
226
|
+
// above). Overview count columns are D..I (Passed·Failed·Pending·N/A·Remain·Total).
|
|
227
|
+
const sumCol = (col, result) => ({ formula: `SUM(${col}${firstDataRow}:${col}${lastDataRow})`, result });
|
|
228
|
+
writeDataRow(rowIdx, [
|
|
229
|
+
'', 'Total',
|
|
230
|
+
sumCol('D', totals.passed),
|
|
231
|
+
sumCol('E', totals.failed),
|
|
232
|
+
sumCol('F', totals.pending),
|
|
233
|
+
sumCol('G', totals.na),
|
|
234
|
+
sumCol('H', totals.remain),
|
|
235
|
+
sumCol('I', totals.total),
|
|
236
|
+
], true);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Append one BM-2-901-13 testcase sheet to the given workbook. Sheet content
|
|
240
|
+
* (layout, summary band, data rows, auto-filter, protection) is identical to
|
|
241
|
+
* the single-sheet `renderXlsx` output — only the sheet name differs.
|
|
242
|
+
*/
|
|
243
|
+
function addTestcaseSheet(wb, sheetName, summary, rows, specLink, sungenVersion) {
|
|
244
|
+
const ws = wb.addWorksheet(sheetName, {
|
|
245
|
+
views: [{ state: 'frozen', ySplit: 0, xSplit: 0 }],
|
|
246
|
+
});
|
|
247
|
+
// -- Column widths matching template_report.xlsx (Sample sheet) --
|
|
248
|
+
ws.columns = [
|
|
249
|
+
{ width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
|
|
250
|
+
{ width: 12.5 }, // B — Screen/Function
|
|
251
|
+
{ width: 15.38 }, // C — Big item
|
|
252
|
+
{ width: 16.25 }, // D — Medium item
|
|
253
|
+
{ width: 17 }, // E — Test Object
|
|
254
|
+
{ width: 25.5 }, // F — Pre-condition
|
|
255
|
+
{ width: 24.88 }, // G — Test Data
|
|
256
|
+
{ width: 50 }, // H — Steps (widened ~2× — steps are the longest cell)
|
|
257
|
+
{ width: 41 }, // I — Expected results
|
|
258
|
+
{ width: 12 }, // J — Priority
|
|
259
|
+
{ width: 13.5 }, // K — Testcase type
|
|
260
|
+
{ width: 15.88 }, // L — Test Result
|
|
261
|
+
{ width: 13.5 }, // M — Executed Date
|
|
262
|
+
{ width: 16.5 }, // N — Test Executor
|
|
263
|
+
{ width: 23 }, // O — Test Environment
|
|
264
|
+
{ width: 29 }, // P — Note
|
|
265
|
+
];
|
|
266
|
+
// -- Top metadata band (rows 1-4) — shared BM-2-901-13 masthead --
|
|
267
|
+
renderReportHeaderBand(wb, ws, `${summary.screen.toUpperCase()} TESTCASE`, sungenVersion);
|
|
268
|
+
// Materialize rows 5..8 so the summary band + row-9 spacer land at the right offsets.
|
|
269
|
+
for (let r = 5; r <= 8; r++)
|
|
270
|
+
ws.getRow(r);
|
|
271
|
+
// -- Row 5: spacer (height auto-fit). --
|
|
272
|
+
// -- Row 6: Summary headers (cols C..H), lavender fill --
|
|
273
|
+
const sumLabels = {
|
|
274
|
+
C: 'Total TCs', D: 'Passed', E: 'Failed', F: 'Pending', G: 'N/A', H: 'Remaining',
|
|
275
|
+
};
|
|
276
|
+
for (const [col, label] of Object.entries(sumLabels)) {
|
|
277
|
+
const c = ws.getCell(`${col}6`);
|
|
278
|
+
c.value = label;
|
|
279
|
+
c.font = { bold: true, size: 10, color: exports.BLACK, name: exports.ARIAL_FONT };
|
|
280
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: exports.LAVENDER };
|
|
281
|
+
c.alignment = { horizontal: 'center', vertical: 'top' };
|
|
282
|
+
c.border = exports.allBordersBlack;
|
|
283
|
+
}
|
|
284
|
+
// No explicit height → Excel auto-fits this row's content.
|
|
285
|
+
// -- Row 7: Counts (cols C..H), live formulas referencing the data area --
|
|
286
|
+
// The data region begins at row 14 — the row immediately AFTER the column-header row (row 13).
|
|
287
|
+
// Start the ranges there (NOT row 15): the first category divider usually sits at row 14 with a
|
|
288
|
+
// BLANK col A (so COUNTA skips it), but a sheet whose first row after the header is real data —
|
|
289
|
+
// no leading divider — would be UNDER-counted by a range that started at 15. Row 14 covers both.
|
|
290
|
+
const COUNT_RANGE_START = 14;
|
|
291
|
+
const COUNT_RANGE_END = 10000;
|
|
292
|
+
const r7Values = {
|
|
293
|
+
C: { formula: `COUNTA(A${COUNT_RANGE_START}:A${COUNT_RANGE_END})`, result: summary.total },
|
|
294
|
+
D: { formula: `COUNTIF(L${COUNT_RANGE_START}:L${COUNT_RANGE_END},"Passed")`, result: summary.passed },
|
|
295
|
+
E: { formula: `COUNTIF(L${COUNT_RANGE_START}:L${COUNT_RANGE_END},"Failed")`, result: summary.failed },
|
|
296
|
+
F: { formula: `COUNTIF(L${COUNT_RANGE_START}:L${COUNT_RANGE_END},"Pending")`, result: summary.pending },
|
|
297
|
+
G: { formula: `COUNTIF(L${COUNT_RANGE_START}:L${COUNT_RANGE_END},"N/A")`, result: summary.na },
|
|
298
|
+
H: { formula: 'C7-D7-G7', result: summary.failed + summary.pending },
|
|
299
|
+
};
|
|
300
|
+
for (const [col, val] of Object.entries(r7Values)) {
|
|
301
|
+
const c = ws.getCell(`${col}7`);
|
|
302
|
+
c.value = val;
|
|
303
|
+
c.font = { size: 10, color: exports.BLACK, name: exports.ARIAL_FONT };
|
|
304
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: exports.WHITE };
|
|
305
|
+
c.alignment = { horizontal: 'center', vertical: 'top' };
|
|
306
|
+
c.border = exports.allBordersBlack;
|
|
307
|
+
if (col === 'H')
|
|
308
|
+
c.numFmt = '#,##0';
|
|
309
|
+
}
|
|
310
|
+
// No explicit height → Excel auto-fits.
|
|
311
|
+
// C8 has no value in the template but still needs a border so the band
|
|
312
|
+
// visually closes underneath "Total TCs".
|
|
313
|
+
const c8 = ws.getCell('C8');
|
|
314
|
+
c8.font = { size: 10, color: exports.BLACK, name: exports.ARIAL_FONT };
|
|
315
|
+
c8.fill = { type: 'pattern', pattern: 'solid', fgColor: exports.WHITE };
|
|
316
|
+
c8.alignment = { horizontal: 'center', vertical: 'top' };
|
|
317
|
+
c8.border = exports.allBordersBlack;
|
|
318
|
+
// -- Row 8: Percentages (cols D..H), formula = count / (Total − N/A) --
|
|
319
|
+
const r8Values = {
|
|
320
|
+
D: { formula: 'IFERROR(D7/(C7-G7),0)', result: summary.passed / Math.max(1, summary.total - summary.na) },
|
|
321
|
+
E: { formula: 'IFERROR(E7/(C7-G7),0)', result: summary.failed / Math.max(1, summary.total - summary.na) },
|
|
322
|
+
F: { formula: 'IFERROR(F7/(C7-G7),0)', result: summary.pending / Math.max(1, summary.total - summary.na) },
|
|
323
|
+
G: { formula: 'IFERROR(G7/(C7-G7),0)', result: summary.na / Math.max(1, summary.total - summary.na) },
|
|
324
|
+
H: { formula: 'IFERROR(H7/(C7-G7),0)', result: (summary.failed + summary.pending) / Math.max(1, summary.total - summary.na) },
|
|
325
|
+
};
|
|
326
|
+
for (const [col, val] of Object.entries(r8Values)) {
|
|
327
|
+
const c = ws.getCell(`${col}8`);
|
|
328
|
+
c.value = val;
|
|
329
|
+
c.font = { size: 10, color: exports.BLACK, name: exports.ARIAL_FONT };
|
|
330
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: exports.WHITE };
|
|
331
|
+
c.alignment = { horizontal: 'center', vertical: 'top' };
|
|
332
|
+
c.border = exports.allBordersBlack;
|
|
333
|
+
c.numFmt = '0%';
|
|
334
|
+
}
|
|
335
|
+
// No explicit height → Excel auto-fits.
|
|
336
|
+
// After rows 1-8 are populated, append a blank row 9 spacer.
|
|
337
|
+
while (ws.rowCount < 9)
|
|
338
|
+
ws.addRow([]);
|
|
339
|
+
const specRow = ws.addRow(['Spec/Design link:', specLink]);
|
|
340
|
+
specRow.getCell(1).font = { bold: true };
|
|
341
|
+
specRow.getCell(2).font = { color: { argb: 'FF1F4E78' }, underline: true };
|
|
342
|
+
ws.addRow([]); // spacer
|
|
343
|
+
const legendRow = ws.addRow(['*: Mandatory']);
|
|
344
|
+
legendRow.getCell(1).font = { italic: true, color: { argb: 'FF808080' } };
|
|
345
|
+
// -- Column header row (bold, filled, wrapped) --
|
|
346
|
+
const headerRow = ws.addRow([
|
|
347
|
+
'TC ID*',
|
|
348
|
+
'Screen/Function',
|
|
349
|
+
'Big item',
|
|
350
|
+
'Medium item',
|
|
351
|
+
'Test Object',
|
|
352
|
+
'Pre-condition',
|
|
353
|
+
'Test Data',
|
|
354
|
+
'Steps*',
|
|
355
|
+
'Expected results*',
|
|
356
|
+
'Priority',
|
|
357
|
+
'Testcase type',
|
|
358
|
+
'Test Result*',
|
|
359
|
+
'Executed Date*',
|
|
360
|
+
'Test Executor*',
|
|
361
|
+
'Test Environment',
|
|
362
|
+
'Note\n(Test evidence, DefectID, Actual result)',
|
|
363
|
+
]);
|
|
364
|
+
headerRow.height = 38;
|
|
365
|
+
headerRow.eachCell((cell) => {
|
|
366
|
+
cell.font = { bold: true, color: { argb: exports.HEADER_FONT } };
|
|
367
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: exports.HEADER_FILL } };
|
|
368
|
+
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
369
|
+
applyBorder(cell);
|
|
370
|
+
});
|
|
371
|
+
// No freeze — the whole sheet scrolls freely (the masthead + summary band + header scroll away
|
|
372
|
+
// with the data). QA asked for this so the wide Steps/Expected columns aren't pinned under a frozen
|
|
373
|
+
// header on smaller screens.
|
|
374
|
+
ws.views = [{ state: 'normal' }];
|
|
375
|
+
// -- Data rows grouped by category --
|
|
376
|
+
const order = ['Accessing', 'GUI', 'Function'];
|
|
377
|
+
const grouped = new Map();
|
|
378
|
+
for (const r of rows) {
|
|
379
|
+
const g = grouped.get(r.category2) || [];
|
|
380
|
+
g.push(r);
|
|
381
|
+
grouped.set(r.category2, g);
|
|
382
|
+
}
|
|
383
|
+
const emittedGroups = new Set();
|
|
384
|
+
function emitGroup(groupName, groupRows) {
|
|
385
|
+
// Column A (TC ID) must stay TRULY blank on a divider — never write '' here. COUNTA counts
|
|
386
|
+
// empty-string cells, so an '' in A made the generated total (=COUNTA(A15:A…)) over-count by
|
|
387
|
+
// one per group divider when recalculated (e.g. in Google Sheets). Set only the group name in B;
|
|
388
|
+
// the lavender band still spans every column via the fill loop below (style only, no value).
|
|
389
|
+
const divider = ws.addRow([]);
|
|
390
|
+
divider.getCell(2).value = groupName;
|
|
391
|
+
divider.getCell(2).font = { bold: true };
|
|
392
|
+
divider.eachCell({ includeEmpty: false }, (cell) => {
|
|
393
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: exports.CATEGORY_FILL } };
|
|
394
|
+
});
|
|
395
|
+
// Fill the full width so the band spans the whole table
|
|
396
|
+
for (let i = 1; i <= exports.COL_COUNT; i++) {
|
|
397
|
+
const c = divider.getCell(i);
|
|
398
|
+
c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: exports.CATEGORY_FILL } };
|
|
399
|
+
applyBorder(c);
|
|
400
|
+
}
|
|
401
|
+
for (const r of groupRows) {
|
|
402
|
+
const row = ws.addRow([
|
|
403
|
+
r.tcId,
|
|
404
|
+
'',
|
|
405
|
+
'',
|
|
406
|
+
'',
|
|
407
|
+
r.category1,
|
|
408
|
+
r.precondition,
|
|
409
|
+
r.testData,
|
|
410
|
+
r.steps,
|
|
411
|
+
r.expectedResults,
|
|
412
|
+
r.priority,
|
|
413
|
+
r.testcaseType,
|
|
414
|
+
r.testResult,
|
|
415
|
+
r.executedDate,
|
|
416
|
+
r.testExecutor,
|
|
417
|
+
r.testEnvironment,
|
|
418
|
+
r.note,
|
|
419
|
+
]);
|
|
420
|
+
row.alignment = { vertical: 'top', wrapText: true };
|
|
421
|
+
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
422
|
+
applyBorder(cell);
|
|
423
|
+
if (colNumber === 1) {
|
|
424
|
+
cell.font = { bold: true };
|
|
425
|
+
}
|
|
426
|
+
if (colNumber === 12) {
|
|
427
|
+
// Test Result — colour-code
|
|
428
|
+
const val = String(cell.value || '');
|
|
429
|
+
if (val === 'Passed') {
|
|
430
|
+
cell.font = { color: { argb: 'FF107C41' }, bold: true };
|
|
431
|
+
}
|
|
432
|
+
else if (val === 'Failed') {
|
|
433
|
+
cell.font = { color: { argb: 'FFC00000' }, bold: true };
|
|
434
|
+
}
|
|
435
|
+
else if (val === 'Pending') {
|
|
436
|
+
cell.font = { color: { argb: 'FF996D00' }, bold: true };
|
|
437
|
+
}
|
|
438
|
+
else if (val === 'N/A') {
|
|
439
|
+
cell.font = { color: { argb: 'FF808080' } };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const groupName of order) {
|
|
446
|
+
const groupRows = grouped.get(groupName);
|
|
447
|
+
if (!groupRows || groupRows.length === 0)
|
|
448
|
+
continue;
|
|
449
|
+
emittedGroups.add(groupName);
|
|
450
|
+
emitGroup(groupName, groupRows);
|
|
451
|
+
}
|
|
452
|
+
for (const [groupName, groupRows] of grouped.entries()) {
|
|
453
|
+
if (emittedGroups.has(groupName))
|
|
454
|
+
continue;
|
|
455
|
+
emitGroup(groupName, groupRows);
|
|
456
|
+
}
|
|
457
|
+
// Auto-filter on the header row so QA can filter by result / priority / category
|
|
458
|
+
ws.autoFilter = {
|
|
459
|
+
from: { row: headerRow.number, column: 1 },
|
|
460
|
+
to: { row: ws.rowCount, column: exports.COL_COUNT },
|
|
461
|
+
};
|
|
462
|
+
// Sheet protection lets the picLocks on the embedded logo take effect
|
|
463
|
+
// (Excel honours image locks only when the sheet is protected). Empty
|
|
464
|
+
// password — QA can run Review → Unprotect Sheet if they need to edit.
|
|
465
|
+
// `objects: false` here is ExcelJS's inverted semantic that writes
|
|
466
|
+
// `objects="1"` in XML (= objects ARE locked).
|
|
467
|
+
ws.sheetProtection = {
|
|
468
|
+
sheet: true,
|
|
469
|
+
objects: false,
|
|
470
|
+
scenarios: false,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// API detail sheet (api-kind units only)
|
|
475
|
+
// Second worksheet appended after Testcases — never alters the Testcases sheet.
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
exports.API_DETAIL_HEADER_FILL = 'FF2E5984'; // dark blue header for differentiation
|
|
478
|
+
exports.API_DETAIL_HEADER_FONT = 'FFFFFFFF'; // white text on dark header
|
|
479
|
+
/**
|
|
480
|
+
* Build BuilderApiDetailRow[] from the apis.yaml catalog + feature-level
|
|
481
|
+
* annotations. Called once per feature file for api-kind units in the delivery
|
|
482
|
+
* pipeline. The tag extractors are injected (see `TagExtractors`).
|
|
483
|
+
*
|
|
484
|
+
* @param catalog Parsed apis.yaml keyed by endpoint name
|
|
485
|
+
* @param scenarios Scenario-level tag arrays from parseFeatureMetadata().scenarios
|
|
486
|
+
* @param extractors Tag-parsing helpers (feature-parser on the CLI side)
|
|
487
|
+
*/
|
|
488
|
+
function buildApiDetailRows(catalog, scenarios, extractors) {
|
|
489
|
+
const { extractApiCallOrder, extractCasesDataset, extractConcurrencyInvariant } = extractors;
|
|
490
|
+
const rows = [];
|
|
491
|
+
for (const [endpointName, entry] of Object.entries(catalog)) {
|
|
492
|
+
const method = (entry.method ?? '').toUpperCase();
|
|
493
|
+
const endpoint = entry.path ?? endpointName;
|
|
494
|
+
const datasource = entry.datasource ?? '';
|
|
495
|
+
// Auth: look for @auth: tag in any scenario that calls this endpoint.
|
|
496
|
+
const authTags = scenarios.flatMap((s) => {
|
|
497
|
+
const calls = extractApiCallOrder(s.tags);
|
|
498
|
+
if (!calls.includes(endpointName))
|
|
499
|
+
return [];
|
|
500
|
+
return s.tags.filter((t) => t.startsWith('@auth:'));
|
|
501
|
+
});
|
|
502
|
+
const uniqueAuth = [...new Set(authTags.map((t) => t.slice('@auth:'.length)))];
|
|
503
|
+
const authDatasource = [datasource, ...uniqueAuth].filter(Boolean).join('; ');
|
|
504
|
+
// Request shape: compose from body + params + encoding.
|
|
505
|
+
const bodyStr = entry.body
|
|
506
|
+
? `body: ${typeof entry.body === 'string' ? entry.body : JSON.stringify(entry.body)}`
|
|
507
|
+
: '';
|
|
508
|
+
const paramsArr = Array.isArray(entry.params) ? entry.params : [];
|
|
509
|
+
const paramsStr = paramsArr.length > 0 ? `params: [${paramsArr.join(', ')}]` : '';
|
|
510
|
+
const requestShape = [bodyStr, paramsStr].filter(Boolean).join('; ') || '—';
|
|
511
|
+
// Expected-status matrix: aggregate @cases dataset labels + expected status
|
|
512
|
+
// from scenarios that call this endpoint. Fall back to catalog expect.status.
|
|
513
|
+
const statusEntries = [];
|
|
514
|
+
for (const sc of scenarios) {
|
|
515
|
+
const calls = extractApiCallOrder(sc.tags);
|
|
516
|
+
if (!calls.includes(endpointName))
|
|
517
|
+
continue;
|
|
518
|
+
const dataset = extractCasesDataset(sc.tags);
|
|
519
|
+
if (dataset) {
|
|
520
|
+
// @cases dataset name as label — actual per-row statuses live in test-data.yaml
|
|
521
|
+
statusEntries.push(`@cases:${dataset}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Show the catalog baseline status plus a pointer to any @cases matrix dataset (the per-row
|
|
525
|
+
// statuses live in test-data; resolving them into this cell is a later enrichment).
|
|
526
|
+
const catalogStatus = entry.expect?.status != null ? String(entry.expect.status) : '';
|
|
527
|
+
const expectedStatusMatrix = [catalogStatus, ...new Set(statusEntries)].filter(Boolean).join('; ') || '—';
|
|
528
|
+
// Flow steps: ordered @api names from flow-tagged scenarios referencing this endpoint.
|
|
529
|
+
const flowStepsSet = new Set();
|
|
530
|
+
for (const sc of scenarios) {
|
|
531
|
+
const calls = extractApiCallOrder(sc.tags);
|
|
532
|
+
if (!calls.includes(endpointName))
|
|
533
|
+
continue;
|
|
534
|
+
// All scenarios show their call order; flow scenarios show multi-step chains.
|
|
535
|
+
if (calls.length > 1) {
|
|
536
|
+
flowStepsSet.add(calls.join(' → '));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const flowSteps = [...flowStepsSet].join('; ') || '—';
|
|
540
|
+
// Concurrency invariant: from @concurrent scenarios calling this endpoint.
|
|
541
|
+
const concurrencyParts = [];
|
|
542
|
+
for (const sc of scenarios) {
|
|
543
|
+
const calls = extractApiCallOrder(sc.tags);
|
|
544
|
+
if (!calls.includes(endpointName))
|
|
545
|
+
continue;
|
|
546
|
+
const inv = extractConcurrencyInvariant(sc.tags);
|
|
547
|
+
if (inv)
|
|
548
|
+
concurrencyParts.push(inv);
|
|
549
|
+
}
|
|
550
|
+
const concurrencyInvariant = concurrencyParts.join('; ') || '—';
|
|
551
|
+
rows.push({
|
|
552
|
+
endpoint,
|
|
553
|
+
method,
|
|
554
|
+
authDatasource,
|
|
555
|
+
requestShape,
|
|
556
|
+
expectedStatusMatrix,
|
|
557
|
+
flowSteps,
|
|
558
|
+
concurrencyInvariant,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return rows;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Append a second "API detail" worksheet to the workbook.
|
|
565
|
+
* Called only for api-kind units; no effect on the Testcases sheet or other sheets.
|
|
566
|
+
*
|
|
567
|
+
* Columns: Endpoint · Method · Auth/Datasource · Request shape ·
|
|
568
|
+
* Expected-status matrix · Flow steps · Concurrency invariant
|
|
569
|
+
*/
|
|
570
|
+
function addApiDetailSheet(wb, detailRows) {
|
|
571
|
+
const ws = wb.addWorksheet('API detail');
|
|
572
|
+
const ARIAL_FONT = 'Arial';
|
|
573
|
+
ws.columns = [
|
|
574
|
+
{ header: 'Endpoint', width: 35 },
|
|
575
|
+
{ header: 'Method', width: 10 },
|
|
576
|
+
{ header: 'Auth / Datasource', width: 22 },
|
|
577
|
+
{ header: 'Request shape', width: 40 },
|
|
578
|
+
{ header: 'Expected-status matrix', width: 30 },
|
|
579
|
+
{ header: 'Flow steps', width: 40 },
|
|
580
|
+
{ header: 'Concurrency invariant', width: 35 },
|
|
581
|
+
];
|
|
582
|
+
// Style the auto-generated header row (row 1).
|
|
583
|
+
const headerRow = ws.getRow(1);
|
|
584
|
+
headerRow.height = 30;
|
|
585
|
+
headerRow.eachCell((cell) => {
|
|
586
|
+
cell.font = { bold: true, color: { argb: exports.API_DETAIL_HEADER_FONT }, name: ARIAL_FONT };
|
|
587
|
+
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: exports.API_DETAIL_HEADER_FILL } };
|
|
588
|
+
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
|
|
589
|
+
applyBorder(cell);
|
|
590
|
+
});
|
|
591
|
+
ws.views = [{ state: 'frozen', ySplit: 1 }];
|
|
592
|
+
for (const r of detailRows) {
|
|
593
|
+
const row = ws.addRow([
|
|
594
|
+
r.endpoint,
|
|
595
|
+
r.method,
|
|
596
|
+
r.authDatasource,
|
|
597
|
+
r.requestShape,
|
|
598
|
+
r.expectedStatusMatrix,
|
|
599
|
+
r.flowSteps,
|
|
600
|
+
r.concurrencyInvariant,
|
|
601
|
+
]);
|
|
602
|
+
row.alignment = { vertical: 'top', wrapText: true };
|
|
603
|
+
row.eachCell({ includeEmpty: true }, (cell) => {
|
|
604
|
+
applyBorder(cell);
|
|
605
|
+
cell.font = { name: ARIAL_FONT };
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
ws.autoFilter = {
|
|
609
|
+
from: { row: 1, column: 1 },
|
|
610
|
+
to: { row: ws.rowCount, column: 7 },
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// High-level entry points — one call builds a whole workbook. These carry the
|
|
615
|
+
// exact behavior of the CLI's `renderXlsx` / `renderXlsxMultiSheet`; the CLI
|
|
616
|
+
// and dashboard wrappers only adapt their domain types and inject opts.
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
/**
|
|
619
|
+
* Build a single-sheet testcase workbook (plus an optional trailing
|
|
620
|
+
* "API detail" sheet). Mirrors the CLI's `renderXlsx` — the caller passes
|
|
621
|
+
* `sheetName: 'Testcases'` for the canonical single-sheet delivery.
|
|
622
|
+
*/
|
|
623
|
+
function buildSingleTestcaseWorkbook(sheet, apiDetail, opts) {
|
|
624
|
+
const wb = new exceljs_1.default.Workbook();
|
|
625
|
+
wb.creator = 'sungen delivery';
|
|
626
|
+
wb.created = new Date();
|
|
627
|
+
addTestcaseSheet(wb, sheet.sheetName, sheet.summary, sheet.rows, sheet.specLink, opts?.sungenVersion);
|
|
628
|
+
if (apiDetail) {
|
|
629
|
+
addApiDetailSheet(wb, apiDetail.rows);
|
|
630
|
+
}
|
|
631
|
+
return wb;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Build a multi-sheet testcase workbook: an Overview roll-up first, then one
|
|
635
|
+
* BM-2-901-13 sheet per entry. Mirrors the CLI's `renderXlsxMultiSheet`.
|
|
636
|
+
*/
|
|
637
|
+
function buildTestcaseWorkbook(sheets, opts) {
|
|
638
|
+
if (sheets.length === 0) {
|
|
639
|
+
// Message kept verbatim from the original wrapper for behavior stability.
|
|
640
|
+
throw new Error('renderXlsxMultiSheet requires at least one locale sheet');
|
|
641
|
+
}
|
|
642
|
+
const wb = new exceljs_1.default.Workbook();
|
|
643
|
+
wb.creator = 'sungen delivery';
|
|
644
|
+
wb.created = new Date();
|
|
645
|
+
// Overview first — a per-sheet roll-up (Passed/Failed/Pending/N/A/Remain/Total)
|
|
646
|
+
// plus a grand-total row, so QA sees the whole picture before the detail sheets.
|
|
647
|
+
addOverviewSheet(wb, sheets, opts?.sungenVersion);
|
|
648
|
+
for (const s of sheets) {
|
|
649
|
+
addTestcaseSheet(wb, s.sheetName, s.summary, s.rows, s.specLink, opts?.sungenVersion);
|
|
650
|
+
}
|
|
651
|
+
return wb;
|
|
652
|
+
}
|
|
653
|
+
//# sourceMappingURL=xlsx-report-builder.js.map
|