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