@sun-asterisk/sungen 2.6.8 → 2.6.11

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 (89) hide show
  1. package/dist/cli/commands/dashboard.d.ts +2 -1
  2. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  3. package/dist/cli/commands/dashboard.js +9 -9
  4. package/dist/cli/commands/dashboard.js.map +1 -1
  5. package/dist/cli/commands/delivery.d.ts.map +1 -1
  6. package/dist/cli/commands/delivery.js +33 -0
  7. package/dist/cli/commands/delivery.js.map +1 -1
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/dashboard/history-store.d.ts +13 -9
  11. package/dist/dashboard/history-store.d.ts.map +1 -1
  12. package/dist/dashboard/history-store.js +19 -28
  13. package/dist/dashboard/history-store.js.map +1 -1
  14. package/dist/dashboard/html-renderer.d.ts +1 -1
  15. package/dist/dashboard/html-renderer.d.ts.map +1 -1
  16. package/dist/dashboard/html-renderer.js +2 -2
  17. package/dist/dashboard/html-renderer.js.map +1 -1
  18. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  19. package/dist/dashboard/snapshot-builder.js +38 -2
  20. package/dist/dashboard/snapshot-builder.js.map +1 -1
  21. package/dist/dashboard/templates/index.html +153 -221
  22. package/dist/exporters/csv-exporter.d.ts +4 -0
  23. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  24. package/dist/exporters/csv-exporter.js +35 -26
  25. package/dist/exporters/csv-exporter.js.map +1 -1
  26. package/dist/exporters/feature-parser.d.ts.map +1 -1
  27. package/dist/exporters/feature-parser.js +16 -4
  28. package/dist/exporters/feature-parser.js.map +1 -1
  29. package/dist/exporters/json-exporter.d.ts.map +1 -1
  30. package/dist/exporters/json-exporter.js +28 -20
  31. package/dist/exporters/json-exporter.js.map +1 -1
  32. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  33. package/dist/exporters/playwright-report-parser.js +22 -5
  34. package/dist/exporters/playwright-report-parser.js.map +1 -1
  35. package/dist/exporters/scenario-merger.d.ts +23 -1
  36. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  37. package/dist/exporters/scenario-merger.js +39 -0
  38. package/dist/exporters/scenario-merger.js.map +1 -1
  39. package/dist/exporters/step-formatter.d.ts +31 -3
  40. package/dist/exporters/step-formatter.d.ts.map +1 -1
  41. package/dist/exporters/step-formatter.js +52 -19
  42. package/dist/exporters/step-formatter.js.map +1 -1
  43. package/dist/exporters/sun-logo.d.ts +10 -0
  44. package/dist/exporters/sun-logo.d.ts.map +1 -0
  45. package/dist/exporters/sun-logo.js +13 -0
  46. package/dist/exporters/sun-logo.js.map +1 -0
  47. package/dist/exporters/test-data-resolver.d.ts +13 -5
  48. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  49. package/dist/exporters/test-data-resolver.js +36 -14
  50. package/dist/exporters/test-data-resolver.js.map +1 -1
  51. package/dist/exporters/types.d.ts +16 -0
  52. package/dist/exporters/types.d.ts.map +1 -1
  53. package/dist/exporters/xlsx-exporter.d.ts +6 -0
  54. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  55. package/dist/exporters/xlsx-exporter.js +204 -100
  56. package/dist/exporters/xlsx-exporter.js.map +1 -1
  57. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  58. package/dist/orchestrator/project-initializer.js +4 -3
  59. package/dist/orchestrator/project-initializer.js.map +1 -1
  60. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  61. package/dist/orchestrator/templates/playwright.config.js +2 -0
  62. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  63. package/dist/orchestrator/templates/playwright.config.ts +2 -0
  64. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  65. package/dist/orchestrator/templates/specs-base.js +1 -5
  66. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  67. package/dist/orchestrator/templates/specs-base.ts +1 -5
  68. package/package.json +1 -1
  69. package/src/cli/commands/dashboard.ts +9 -9
  70. package/src/cli/commands/delivery.ts +30 -0
  71. package/src/cli/index.ts +1 -1
  72. package/src/dashboard/history-store.ts +22 -28
  73. package/src/dashboard/html-renderer.ts +6 -2
  74. package/src/dashboard/snapshot-builder.ts +36 -2
  75. package/src/dashboard/templates/index.html +153 -221
  76. package/src/dashboard/types.ts +1 -1
  77. package/src/exporters/csv-exporter.ts +44 -27
  78. package/src/exporters/feature-parser.ts +27 -8
  79. package/src/exporters/json-exporter.ts +31 -21
  80. package/src/exporters/playwright-report-parser.ts +23 -5
  81. package/src/exporters/scenario-merger.ts +65 -1
  82. package/src/exporters/step-formatter.ts +48 -23
  83. package/src/exporters/sun-logo.ts +10 -0
  84. package/src/exporters/test-data-resolver.ts +37 -13
  85. package/src/exporters/types.ts +18 -1
  86. package/src/exporters/xlsx-exporter.ts +216 -102
  87. package/src/orchestrator/project-initializer.ts +4 -3
  88. package/src/orchestrator/templates/playwright.config.ts +2 -0
  89. package/src/orchestrator/templates/specs-base.ts +1 -5
@@ -8,7 +8,11 @@ import { parse as parseYaml } from 'yaml';
8
8
 
9
9
  /**
10
10
  * Load test-data.yaml into a flat key→value map.
11
- * Returns empty map if file missing.
11
+ * Nested objects are flattened with dot notation so flow files like
12
+ * kudos:
13
+ * recipient: "An"
14
+ * become { "kudos.recipient": "An" }, matching {{kudos.recipient}} refs.
15
+ * Arrays are JSON-stringified. Returns empty map if file missing.
12
16
  */
13
17
  export function loadTestData(testDataFilePath: string): Record<string, string> {
14
18
  if (!fs.existsSync(testDataFilePath)) return {};
@@ -16,38 +20,58 @@ export function loadTestData(testDataFilePath: string): Record<string, string> {
16
20
  const parsed = parseYaml(content);
17
21
  if (!parsed || typeof parsed !== 'object') return {};
18
22
 
19
- // Flatten shallow object to string values
20
23
  const result: Record<string, string> = {};
21
- for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
24
+ flatten(parsed as Record<string, unknown>, '', result);
25
+ return result;
26
+ }
27
+
28
+ function flatten(obj: Record<string, unknown>, prefix: string, out: Record<string, string>): void {
29
+ for (const [key, value] of Object.entries(obj)) {
22
30
  if (value === null || value === undefined) continue;
23
- if (typeof value === 'object') continue; // skip nested structures
24
- result[key] = String(value);
31
+ const path = prefix ? `${prefix}.${key}` : key;
32
+ if (Array.isArray(value)) {
33
+ out[path] = JSON.stringify(value);
34
+ } else if (typeof value === 'object') {
35
+ flatten(value as Record<string, unknown>, path, out);
36
+ } else {
37
+ out[path] = String(value);
38
+ }
25
39
  }
26
- return result;
27
40
  }
28
41
 
29
42
  /**
30
- * Format a list of variable references into a "key: value; key2: value2" string
31
- * suitable for the CSV Test Data column.
43
+ * Format a list of variable references into a "key: value" string.
32
44
  *
33
- * Long values (>80 chars) are truncated with "…".
45
+ * - `separator` controls how pairs are joined. Default `'; '` keeps CSV/XLSX
46
+ * cells on one line. Pass `'\n'` for surfaces that render multi-line text
47
+ * (e.g. the dashboard modal uses whitespace-pre-wrap).
48
+ * - Test data is never truncated by default — values represent real test
49
+ * inputs and clients need to see them in full. Pass a finite `maxLen` only
50
+ * if the downstream surface requires a cap (e.g. terminal preview).
34
51
  */
35
52
  export function formatTestData(
36
53
  referencedVars: string[],
37
- testData: Record<string, string>
54
+ testData: Record<string, string>,
55
+ maxLen: number = Infinity,
56
+ separator: string = '; ',
38
57
  ): string {
39
58
  if (referencedVars.length === 0) return '';
59
+ // When joining with newlines (dashboard multi-line view), prefix each line
60
+ // with a bullet point so the list scans easily. CSV-style "; " join skips
61
+ // the bullet — single-cell text shouldn't have list markers.
62
+ const useBullet = separator.includes('\n');
40
63
  const pairs: string[] = [];
41
64
  for (const key of referencedVars) {
42
65
  const value = testData[key];
43
66
  if (value === undefined) continue;
44
- pairs.push(`${key}: ${truncate(value, 80)}`);
67
+ const line = `${key}: ${truncate(value, maxLen)}`;
68
+ pairs.push(useBullet ? `• ${line}` : line);
45
69
  }
46
- return pairs.join('; ');
70
+ return pairs.join(separator);
47
71
  }
48
72
 
49
73
  function truncate(s: string, max: number): string {
50
- if (s.length <= max) return s;
74
+ if (!isFinite(max) || s.length <= max) return s;
51
75
  return s.substring(0, max - 1) + '…';
52
76
  }
53
77
 
@@ -15,7 +15,7 @@ export interface TestCaseRow {
15
15
  testData: string; // semicolon-separated key: value
16
16
  steps: string; // numbered steps
17
17
  expectedResults: string; // numbered expected results
18
- priority: string; // Critical | High | Normal | Low
18
+ priority: string; // High | Normal | Low
19
19
  testcaseType: string; // Auto | Manual | Not compiled
20
20
  testResult: string; // Passed | Failed | Pending | N/A
21
21
  executedDate: string; // dd/mm/yyyy
@@ -59,9 +59,20 @@ export interface FeatureMetadata {
59
59
  featureName: string; // e.g., "Create Kudo Modal"
60
60
  featurePath?: string; // e.g., "/kudos"
61
61
  featureTags: string[]; // Feature-level tags
62
+ /** Background block classified by bucket. Shared across all runnable
63
+ * scenarios — the merger is responsible for prepending these. */
64
+ backgroundGivenSteps: string[];
65
+ backgroundWhenSteps: string[];
66
+ backgroundThenSteps: string[];
67
+ backgroundOrderedSteps: OrderedStep[];
62
68
  scenarios: ScenarioMetadata[];
63
69
  }
64
70
 
71
+ export interface OrderedStep {
72
+ text: string;
73
+ bucket: 'given' | 'when' | 'then';
74
+ }
75
+
65
76
  export interface ScenarioMetadata {
66
77
  name: string; // Full scenario name (with VP prefix)
67
78
  tags: string[]; // Scenario-level tags (merged with feature tags)
@@ -71,6 +82,12 @@ export interface ScenarioMetadata {
71
82
  rawGivenSteps: string[]; // Raw Given/And-after-Given step text
72
83
  rawWhenSteps: string[]; // Raw When/And-after-When step text
73
84
  rawThenSteps: string[]; // Raw Then/And-after-Then step text
85
+ /**
86
+ * Steps in original chronological order with each step labelled by its
87
+ * effective bucket (And/But inherits from the prior explicit keyword).
88
+ * Use this when ordering matters — e.g. mid-scenario `Given` after `When`.
89
+ */
90
+ orderedSteps: OrderedStep[];
74
91
  }
75
92
 
76
93
  /**
@@ -12,14 +12,16 @@
12
12
  import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import ExcelJS from 'exceljs';
15
+ import JSZip from 'jszip';
15
16
  import { ScreenSummary, TestCaseRow } from './types';
16
17
  import { getPackageVersion } from './package-info';
18
+ import { SUN_LOGO_PNG_BASE64 } from './sun-logo';
17
19
 
18
20
  const COL_COUNT = 16;
19
- const HEADER_FILL = 'FF1F4E78'; // deep blue band
20
- const HEADER_FONT = 'FFFFFFFF';
21
+ const HEADER_FILL = 'FFD9D2E9'; // lavender matches the summary-header band on row 6
22
+ const HEADER_FONT = 'FF000000'; // black text reads better on the light lavender
21
23
  const META_FILL = 'FFDCE6F1'; // soft blue for meta rows
22
- const CATEGORY_FILL = 'FFFFF2CC'; // soft yellow for group dividers
24
+ const CATEGORY_FILL = 'FFD9D2E9'; // same lavender for group dividers (Accessing / GUI / Function)
23
25
  const BORDER_COLOR = 'FFBFBFBF';
24
26
 
25
27
  type AnyCell = ExcelJS.Cell;
@@ -51,111 +53,180 @@ export function renderXlsx(
51
53
  return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
52
54
  })();
53
55
 
54
- const total = summary.total || 1;
55
- const pct = (n: number) => `${Math.round((n / total) * 100)}%`;
56
-
57
- // -- Column widths (matches CSV column order) --
56
+ // -- Column widths matching template_report.xlsx (Sample sheet) --
58
57
  ws.columns = [
59
- { width: 18 }, // TC ID
60
- { width: 42 }, // Category 1
61
- { width: 14 }, // Category 2
62
- { width: 26 }, // Category 3
63
- { width: 14 }, // Category 4
64
- { width: 36 }, // Pre-condition
65
- { width: 30 }, // Test Data
66
- { width: 46 }, // Steps
67
- { width: 52 }, // Expected results
68
- { width: 10 }, // Priority
69
- { width: 14 }, // Testcase type
70
- { width: 14 }, // Test Result
71
- { width: 14 }, // Executed Date
72
- { width: 16 }, // Test Executor
73
- { width: 38 }, // Test Environment
74
- { width: 40 }, // Note
58
+ { width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
59
+ { width: 12.5 }, // B — Screen/Function
60
+ { width: 15.38 }, // C — Big item
61
+ { width: 16.25 }, // D — Medium item
62
+ { width: 17 }, // E — Test Object
63
+ { width: 25.5 }, // F — Pre-condition
64
+ { width: 24.88 }, // G — Test Data
65
+ { width: 24.88 }, // H — Steps
66
+ { width: 41 }, // I — Expected results
67
+ { width: 12 }, // J — Priority
68
+ { width: 13.5 }, // K — Testcase type
69
+ { width: 15.88 }, // L — Test Result
70
+ { width: 13.5 }, // M — Executed Date
71
+ { width: 16.5 }, // N — Test Executor
72
+ { width: 23 }, // O — Test Environment
73
+ { width: 29 }, // P — Note
75
74
  ];
76
75
 
77
- // -- Top metadata band (rows 1-4) --
76
+ // -- Top metadata band (rows 1-4) — 1:1 with template_report.xlsx --
77
+ const TIMES = 'Times New Roman';
78
+ const ARIAL_FONT = 'Arial';
79
+ const BLACK = { argb: 'FF000000' };
80
+ const WHITE = { argb: 'FFFFFFFF' };
81
+ const LAVENDER = { argb: 'FFD9D2E9' };
82
+ const thinBlack = { style: 'thin' as const, color: BLACK };
83
+ const allBordersBlack = { top: thinBlack, left: thinBlack, bottom: thinBlack, right: thinBlack };
84
+
85
+ // Ensure rows 1..8 exist before merging.
86
+ for (let r = 1; r <= 8; r++) ws.getRow(r);
87
+
88
+ ws.mergeCells('A1:C3');
89
+ ws.mergeCells('D1:F3');
90
+ ws.mergeCells('G1:H1');
91
+ ws.mergeCells('G2:H2');
92
+ ws.mergeCells('G3:H3');
93
+ ws.mergeCells('A4:F4');
94
+ ws.mergeCells('G4:H4');
95
+
96
+ // A1 (logo band, merged A1:C3) — border + embedded Sun* logo (base64 inline).
97
+ const a1 = ws.getCell('A1');
98
+ a1.alignment = { horizontal: 'center', vertical: 'middle' };
99
+ a1.border = allBordersBlack;
100
+
101
+ try {
102
+ const imageId = wb.addImage({
103
+ buffer: Buffer.from(SUN_LOGO_PNG_BASE64, 'base64') as unknown as ExcelJS.Buffer,
104
+ extension: 'png',
105
+ });
106
+ // Centre a fixed-size 90×51 logo inside A1:C3 using absolute EMU offsets.
107
+ // Col widths (A=20, B=12.5, C=15.38) → pixels via Excel's `w*7+5` rule:
108
+ // A=145, B=92.5, C=112.66 ⇒ A1:C3 ≈ 350 px wide.
109
+ // Left padding for a 90-px image = (350−90)/2 ≈ 130 px = 1,237,500 EMU.
110
+ // 3 default rows × 20 px = 60 px; top padding ≈ 4.5 px ≈ 42,862 EMU.
111
+ ws.addImage(imageId, {
112
+ tl: { nativeCol: 0, nativeColOff: 1237500, nativeRow: 0, nativeRowOff: 42862 } as unknown as ExcelJS.Anchor,
113
+ ext: { width: 90, height: 51 },
114
+ editAs: 'oneCell',
115
+ } as unknown as ExcelJS.ImageRange);
116
+ } catch { /* logo is decorative — never block export */ }
117
+
118
+ // D1 (TESTCASE title)
78
119
  const titleLabel = `${summary.screen.toUpperCase()} TESTCASE`;
79
- const titleRow = ws.addRow(['', '', '', titleLabel, '', '', 'No: BM-2-901-13']);
80
- titleRow.getCell(4).font = { bold: true, size: 16 };
81
- titleRow.getCell(4).alignment = { horizontal: 'center', vertical: 'middle' };
82
- ws.mergeCells(titleRow.number, 4, titleRow.number, 6);
83
- titleRow.getCell(7).font = { bold: true };
84
- titleRow.height = 22;
85
-
86
- const versionRow = ws.addRow(['', '', '', '', '', '', `Version: ${getPackageVersion()}`]);
87
- versionRow.getCell(7).font = { bold: true };
88
- const dateRow = ws.addRow(['', '', '', '', '', '', `Issue Date: ${issueDate}`]);
89
- dateRow.getCell(7).font = { bold: true };
90
- const companyRow = ws.addRow([
91
- 'SUN ASTERISK VIETNAM CO., LTD',
92
- '',
93
- '',
94
- '',
95
- '',
96
- '',
97
- 'ISO/IEC 27001:2022 & ISO 9001:2015',
98
- ]);
99
- companyRow.getCell(1).font = { bold: true };
100
- companyRow.getCell(7).font = { bold: true };
120
+ const d1 = ws.getCell('D1');
121
+ d1.value = titleLabel;
122
+ d1.font = { bold: true, size: 18, name: TIMES };
123
+ d1.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
124
+ d1.border = allBordersBlack;
101
125
 
102
- ws.addRow([]); // spacer
126
+ // G1 — No: BM-2-901-13
127
+ const g1 = ws.getCell('G1');
128
+ g1.value = 'No: BM-2-901-13';
129
+ g1.font = { size: 12, name: TIMES };
130
+ g1.alignment = { vertical: 'middle' };
131
+ g1.border = { top: thinBlack, left: thinBlack, right: thinBlack };
103
132
 
104
- // -- Summary band --
105
- const summaryHeader = ws.addRow([
106
- '',
107
- '',
108
- 'Total TCs',
109
- 'Passed',
110
- 'Failed',
111
- 'Pending',
112
- 'N/A',
113
- 'Remaining',
114
- ]);
115
- for (let i = 3; i <= 8; i++) {
116
- const c = summaryHeader.getCell(i);
117
- c.font = { bold: true, color: { argb: HEADER_FONT } };
118
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: HEADER_FILL } };
119
- c.alignment = { horizontal: 'center', vertical: 'middle' };
120
- applyBorder(c);
133
+ // G2 Version
134
+ const g2 = ws.getCell('G2');
135
+ g2.value = `Version: ${getPackageVersion()}`;
136
+ g2.font = { size: 12, name: TIMES };
137
+ g2.alignment = { vertical: 'middle' };
138
+ g2.border = { left: thinBlack, right: thinBlack };
139
+
140
+ // G3 — Issue Date
141
+ const g3 = ws.getCell('G3');
142
+ g3.value = `Issue Date: ${issueDate}`;
143
+ g3.font = { size: 12, name: TIMES };
144
+ g3.alignment = { vertical: 'middle' };
145
+ g3.border = { left: thinBlack, right: thinBlack, bottom: thinBlack };
146
+
147
+ // A4 SUN ASTERISK VIETNAM CO., LTD
148
+ const a4 = ws.getCell('A4');
149
+ a4.value = 'SUN ASTERISK VIETNAM CO., LTD';
150
+ a4.font = { bold: true, size: 10, name: TIMES };
151
+ a4.alignment = { vertical: 'middle' };
152
+ a4.border = allBordersBlack;
153
+
154
+ // G4 — ISO/IEC ...
155
+ const g4 = ws.getCell('G4');
156
+ g4.value = 'ISO/IEC 27001:2022 & ISO 9001:2015';
157
+ g4.font = { bold: true, size: 10, name: TIMES };
158
+ g4.alignment = { horizontal: 'right', vertical: 'middle', wrapText: true };
159
+ g4.border = allBordersBlack;
160
+
161
+ // -- Row 5: spacer (height auto-fit). --
162
+
163
+ // -- Row 6: Summary headers (cols C..H), lavender fill --
164
+ const sumLabels: Record<string, string> = {
165
+ C: 'Total TCs', D: 'Passed', E: 'Failed', F: 'Pending', G: 'N/A', H: 'Remaining',
166
+ };
167
+ for (const [col, label] of Object.entries(sumLabels)) {
168
+ const c = ws.getCell(`${col}6`);
169
+ c.value = label;
170
+ c.font = { bold: true, size: 10, color: BLACK, name: ARIAL_FONT };
171
+ c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
172
+ c.alignment = { horizontal: 'center', vertical: 'top' };
173
+ c.border = allBordersBlack;
121
174
  }
175
+ // No explicit height → Excel auto-fits this row's content.
122
176
 
123
- const summaryCounts = ws.addRow([
124
- '',
125
- '',
126
- summary.total,
127
- summary.passed,
128
- summary.failed,
129
- summary.pending,
130
- summary.na,
131
- summary.pending + summary.na,
132
- ]);
133
- for (let i = 3; i <= 8; i++) {
134
- const c = summaryCounts.getCell(i);
135
- c.font = { bold: true };
136
- c.alignment = { horizontal: 'center' };
137
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: META_FILL } };
138
- applyBorder(c);
177
+ // -- Row 7: Counts (cols C..H), live formulas referencing the data area --
178
+ // Data table starts at row 15 (col header at row 14, divider at row 14? actually
179
+ // header at row 13 + divider at 14 + first data at 15). The formulas read col L
180
+ // (Test Result) for status counts, col A (TC ID) for total via COUNTA.
181
+ const COUNT_RANGE_END = 10000;
182
+ const r7Values: Record<string, ExcelJS.CellValue> = {
183
+ C: { formula: `COUNTA(A15:A${COUNT_RANGE_END})`, result: summary.total } as ExcelJS.CellFormulaValue,
184
+ D: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"Passed")`, result: summary.passed } as ExcelJS.CellFormulaValue,
185
+ E: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"Failed")`, result: summary.failed } as ExcelJS.CellFormulaValue,
186
+ F: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"Pending")`, result: summary.pending } as ExcelJS.CellFormulaValue,
187
+ G: { formula: `COUNTIF(L15:L${COUNT_RANGE_END},"N/A")`, result: summary.na } as ExcelJS.CellFormulaValue,
188
+ H: { formula: 'C7-D7-G7', result: summary.failed + summary.pending } as ExcelJS.CellFormulaValue,
189
+ };
190
+ for (const [col, val] of Object.entries(r7Values)) {
191
+ const c = ws.getCell(`${col}7`);
192
+ c.value = val;
193
+ c.font = { size: 10, color: BLACK, name: ARIAL_FONT };
194
+ c.fill = { type: 'pattern', pattern: 'solid', fgColor: WHITE };
195
+ c.alignment = { horizontal: 'center', vertical: 'top' };
196
+ c.border = allBordersBlack;
197
+ if (col === 'H') c.numFmt = '#,##0';
139
198
  }
199
+ // No explicit height → Excel auto-fits.
140
200
 
141
- const summaryPct = ws.addRow([
142
- '',
143
- '',
144
- '',
145
- pct(summary.passed),
146
- pct(summary.failed),
147
- pct(summary.pending),
148
- pct(summary.na),
149
- pct(summary.pending + summary.na),
150
- ]);
151
- for (let i = 4; i <= 8; i++) {
152
- const c = summaryPct.getCell(i);
153
- c.alignment = { horizontal: 'center' };
154
- c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: META_FILL } };
155
- applyBorder(c);
201
+ // C8 has no value in the template but still needs a border so the band
202
+ // visually closes underneath "Total TCs".
203
+ const c8 = ws.getCell('C8');
204
+ c8.font = { size: 10, color: BLACK, name: ARIAL_FONT };
205
+ c8.fill = { type: 'pattern', pattern: 'solid', fgColor: WHITE };
206
+ c8.alignment = { horizontal: 'center', vertical: 'top' };
207
+ c8.border = allBordersBlack;
208
+
209
+ // -- Row 8: Percentages (cols D..H), formula = count / (Total − N/A) --
210
+ const r8Values: Record<string, ExcelJS.CellValue> = {
211
+ D: { formula: 'IFERROR(D7/(C7-G7),0)', result: summary.passed / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
212
+ E: { formula: 'IFERROR(E7/(C7-G7),0)', result: summary.failed / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
213
+ F: { formula: 'IFERROR(F7/(C7-G7),0)', result: summary.pending / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
214
+ G: { formula: 'IFERROR(G7/(C7-G7),0)', result: summary.na / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
215
+ H: { formula: 'IFERROR(H7/(C7-G7),0)', result: (summary.failed + summary.pending) / Math.max(1, summary.total - summary.na) } as ExcelJS.CellFormulaValue,
216
+ };
217
+ for (const [col, val] of Object.entries(r8Values)) {
218
+ const c = ws.getCell(`${col}8`);
219
+ c.value = val;
220
+ c.font = { size: 10, color: BLACK, name: ARIAL_FONT };
221
+ c.fill = { type: 'pattern', pattern: 'solid', fgColor: WHITE };
222
+ c.alignment = { horizontal: 'center', vertical: 'top' };
223
+ c.border = allBordersBlack;
224
+ c.numFmt = '0%';
156
225
  }
226
+ // No explicit height → Excel auto-fits.
157
227
 
158
- ws.addRow([]); // spacer
228
+ // After rows 1-8 are populated, append a blank row 9 spacer.
229
+ while (ws.rowCount < 9) ws.addRow([]);
159
230
 
160
231
  const specRow = ws.addRow(['Spec/Design link:', specLink]);
161
232
  specRow.getCell(1).font = { bold: true };
@@ -169,10 +240,10 @@ export function renderXlsx(
169
240
  // -- Column header row (bold, filled, wrapped) --
170
241
  const headerRow = ws.addRow([
171
242
  'TC ID*',
172
- '{Category 1}',
173
- '{Category 2}',
174
- '{Category 3}',
175
- '{Category 4}',
243
+ 'Screen/Function',
244
+ 'Big item',
245
+ 'Medium item',
246
+ 'Test Object',
176
247
  'Pre-condition',
177
248
  'Test Data',
178
249
  'Steps*',
@@ -222,10 +293,10 @@ export function renderXlsx(
222
293
  for (const r of groupRows) {
223
294
  const row = ws.addRow([
224
295
  r.tcId,
225
- r.category1,
226
296
  '',
227
297
  '',
228
298
  '',
299
+ r.category1,
229
300
  r.precondition,
230
301
  r.testData,
231
302
  r.steps,
@@ -278,6 +349,17 @@ export function renderXlsx(
278
349
  to: { row: ws.rowCount, column: COL_COUNT },
279
350
  };
280
351
 
352
+ // Sheet protection lets the picLocks on the embedded logo take effect
353
+ // (Excel honours image locks only when the sheet is protected). Empty
354
+ // password — QA can run Review → Unprotect Sheet if they need to edit.
355
+ // `objects: false` here is ExcelJS's inverted semantic that writes
356
+ // `objects="1"` in XML (= objects ARE locked).
357
+ (ws as unknown as { sheetProtection: object }).sheetProtection = {
358
+ sheet: true,
359
+ objects: false,
360
+ scenarios: false,
361
+ };
362
+
281
363
  return wb;
282
364
  }
283
365
 
@@ -293,9 +375,41 @@ export async function writeXlsx(
293
375
  const outDir = path.join(cwd, 'qa', 'deliverables');
294
376
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
295
377
  const outPath = path.join(outDir, `${screen}-testcases.xlsx`);
296
- await wb.xlsx.writeFile(outPath);
378
+ const buffer = await wb.xlsx.writeBuffer();
379
+ const locked = await lockEmbeddedImages(Buffer.from(buffer as ArrayBuffer));
380
+ fs.writeFileSync(outPath, locked);
297
381
  return outPath;
298
382
  }
299
383
 
384
+ /**
385
+ * Post-process the workbook buffer: extend `<a:picLocks>` on every drawing
386
+ * with `noMove`, `noResize`, `noSelect`. Combined with the sheet protection
387
+ * above, the embedded logo can't be dragged, resized, or selected via the UI.
388
+ */
389
+ export async function lockEmbeddedImages(buffer: Buffer): Promise<Buffer> {
390
+ const zip = await JSZip.loadAsync(buffer);
391
+ const drawingFiles = Object.keys(zip.files).filter(
392
+ (name) => /^xl\/drawings\/drawing\d+\.xml$/.test(name)
393
+ );
394
+ for (const name of drawingFiles) {
395
+ const file = zip.file(name);
396
+ if (!file) continue;
397
+ const xml = await file.async('string');
398
+ const patched = xml.replace(
399
+ /<a:picLocks([^/]*)\/>/g,
400
+ (_match, attrs: string) => {
401
+ const has = (k: string) => new RegExp(`\\b${k}=`).test(attrs);
402
+ const additions = ['noMove', 'noResize', 'noSelect']
403
+ .filter((k) => !has(k))
404
+ .map((k) => ` ${k}="1"`)
405
+ .join('');
406
+ return `<a:picLocks${attrs}${additions}/>`;
407
+ }
408
+ );
409
+ zip.file(name, patched);
410
+ }
411
+ return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
412
+ }
413
+
300
414
  void applyBorder;
301
415
  void ({} as AnyRow);
@@ -450,8 +450,9 @@ export class ProjectInitializer {
450
450
  console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
451
451
  execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
452
452
 
453
- console.log('\n🎭 Installing Playwright browsers...\n');
454
- execSync('npx playwright install', execOpts);
453
+ console.log('\n🎭 Installing Chromium (default browser)...\n');
454
+ execSync('npx playwright install --with-deps chromium', execOpts);
455
+ console.log('\n💡 To install other browsers: npm run install:browsers -- firefox webkit\n');
455
456
  }
456
457
 
457
458
  /**
@@ -477,7 +478,7 @@ export class ProjectInitializer {
477
478
  'test:ui': 'playwright test specs/generated/ --ui',
478
479
  'report': 'playwright show-report',
479
480
  'generate': 'sungen generate --all',
480
- 'install:browsers': 'npx playwright install chromium',
481
+ 'install:browsers': 'npx playwright install',
481
482
  };
482
483
 
483
484
  let added = 0;
@@ -13,6 +13,8 @@ import { defineConfig, devices } from '@playwright/test';
13
13
  */
14
14
  export default defineConfig({
15
15
  testDir: './specs/generated',
16
+ /* Output directory for test artifacts (screenshots, videos, traces). */
17
+ outputDir: './test-results',
16
18
  /* Run tests in files in parallel */
17
19
  fullyParallel: true,
18
20
  /* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -66,14 +66,10 @@ const test = base.extend<{
66
66
  }>({
67
67
  screenshotOnFailure: [false, { option: true }],
68
68
 
69
- page: async ({ browser, storageState }, use) => {
70
- const context = storageState
71
- ? await browser.newContext({ storageState })
72
- : await browser.newContext();
69
+ page: async ({ context }, use) => {
73
70
  const page = await context.newPage();
74
71
  await use(page);
75
72
  await page.close();
76
- await context.close();
77
73
  },
78
74
 
79
75
  _autoScreenshot: [async ({ page, screenshotOnFailure }, use, testInfo) => {