@sun-asterisk/sungen 2.4.5 → 2.4.6

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 (129) hide show
  1. package/dist/cli/commands/delivery.d.ts +7 -0
  2. package/dist/cli/commands/delivery.d.ts.map +1 -0
  3. package/dist/cli/commands/delivery.js +348 -0
  4. package/dist/cli/commands/delivery.js.map +1 -0
  5. package/dist/cli/commands/update.d.ts.map +1 -1
  6. package/dist/cli/commands/update.js +64 -1
  7. package/dist/cli/commands/update.js.map +1 -1
  8. package/dist/cli/index.js +4 -2
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/exporters/csv-exporter.d.ts +32 -0
  11. package/dist/exporters/csv-exporter.d.ts.map +1 -0
  12. package/dist/exporters/csv-exporter.js +311 -0
  13. package/dist/exporters/csv-exporter.js.map +1 -0
  14. package/dist/exporters/feature-parser.d.ts +48 -0
  15. package/dist/exporters/feature-parser.d.ts.map +1 -0
  16. package/dist/exporters/feature-parser.js +178 -0
  17. package/dist/exporters/feature-parser.js.map +1 -0
  18. package/dist/exporters/package-info.d.ts +9 -0
  19. package/dist/exporters/package-info.d.ts.map +1 -0
  20. package/dist/exporters/package-info.js +73 -0
  21. package/dist/exporters/package-info.js.map +1 -0
  22. package/dist/exporters/playwright-report-parser.d.ts +21 -0
  23. package/dist/exporters/playwright-report-parser.d.ts.map +1 -0
  24. package/dist/exporters/playwright-report-parser.js +184 -0
  25. package/dist/exporters/playwright-report-parser.js.map +1 -0
  26. package/dist/exporters/scenario-merger.d.ts +21 -0
  27. package/dist/exporters/scenario-merger.d.ts.map +1 -0
  28. package/dist/exporters/scenario-merger.js +51 -0
  29. package/dist/exporters/scenario-merger.js.map +1 -0
  30. package/dist/exporters/spec-parser.d.ts +20 -0
  31. package/dist/exporters/spec-parser.d.ts.map +1 -0
  32. package/dist/exporters/spec-parser.js +259 -0
  33. package/dist/exporters/spec-parser.js.map +1 -0
  34. package/dist/exporters/step-formatter.d.ts +32 -0
  35. package/dist/exporters/step-formatter.d.ts.map +1 -0
  36. package/dist/exporters/step-formatter.js +76 -0
  37. package/dist/exporters/step-formatter.js.map +1 -0
  38. package/dist/exporters/test-data-resolver.d.ts +20 -0
  39. package/dist/exporters/test-data-resolver.d.ts.map +1 -0
  40. package/dist/exporters/test-data-resolver.js +96 -0
  41. package/dist/exporters/test-data-resolver.js.map +1 -0
  42. package/dist/exporters/types.d.ts +104 -0
  43. package/dist/exporters/types.d.ts.map +1 -0
  44. package/dist/exporters/types.js +6 -0
  45. package/dist/exporters/types.js.map +1 -0
  46. package/dist/exporters/xlsx-exporter.d.ts +19 -0
  47. package/dist/exporters/xlsx-exporter.d.ts.map +1 -0
  48. package/dist/exporters/xlsx-exporter.js +309 -0
  49. package/dist/exporters/xlsx-exporter.js.map +1 -0
  50. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  51. package/dist/generators/test-generator/utils/selector-resolver.js +26 -0
  52. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  53. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  54. package/dist/orchestrator/ai-rules-updater.js +12 -0
  55. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  56. package/dist/orchestrator/project-initializer.d.ts +12 -1
  57. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  58. package/dist/orchestrator/project-initializer.js +84 -64
  59. package/dist/orchestrator/project-initializer.js.map +1 -1
  60. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  61. package/dist/orchestrator/screen-manager.js +2 -0
  62. package/dist/orchestrator/screen-manager.js.map +1 -1
  63. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +15 -17
  64. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +7 -5
  65. package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +71 -0
  66. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +27 -0
  67. package/dist/orchestrator/templates/ai-instructions/claude-config.md +12 -2
  68. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +142 -0
  69. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +100 -0
  70. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +73 -0
  71. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +103 -0
  72. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
  73. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
  74. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -15
  75. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -4
  76. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +71 -0
  77. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -14
  78. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
  79. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +142 -0
  80. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +100 -0
  81. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +73 -0
  82. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +103 -0
  83. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
  84. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +22 -0
  85. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  86. package/dist/orchestrator/templates/playwright.config.js +6 -1
  87. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  88. package/dist/orchestrator/templates/playwright.config.ts +6 -1
  89. package/package.json +2 -1
  90. package/src/cli/commands/delivery.ts +348 -0
  91. package/src/cli/commands/update.ts +84 -2
  92. package/src/cli/index.ts +4 -2
  93. package/src/exporters/csv-exporter.ts +304 -0
  94. package/src/exporters/feature-parser.ts +168 -0
  95. package/src/exporters/package-info.ts +35 -0
  96. package/src/exporters/playwright-report-parser.ts +168 -0
  97. package/src/exporters/scenario-merger.ts +63 -0
  98. package/src/exporters/spec-parser.ts +247 -0
  99. package/src/exporters/step-formatter.ts +80 -0
  100. package/src/exporters/test-data-resolver.ts +59 -0
  101. package/src/exporters/types.ts +112 -0
  102. package/src/exporters/xlsx-exporter.ts +301 -0
  103. package/src/generators/test-generator/utils/selector-resolver.ts +26 -0
  104. package/src/orchestrator/ai-rules-updater.ts +12 -0
  105. package/src/orchestrator/project-initializer.ts +103 -70
  106. package/src/orchestrator/screen-manager.ts +2 -0
  107. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +15 -17
  108. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +7 -5
  109. package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +71 -0
  110. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +27 -0
  111. package/src/orchestrator/templates/ai-instructions/claude-config.md +12 -2
  112. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +142 -0
  113. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +100 -0
  114. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +73 -0
  115. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +103 -0
  116. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
  117. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
  118. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -15
  119. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -4
  120. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +71 -0
  121. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -14
  122. package/src/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
  123. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +142 -0
  124. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +100 -0
  125. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +73 -0
  126. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +103 -0
  127. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
  128. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +22 -0
  129. package/src/orchestrator/templates/playwright.config.ts +6 -1
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Assemble CSV rows from merged scenarios + test data + playwright results.
3
+ * Outputs a CSV file matching the BM-2-901-13 test case template format.
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { getPackageVersion } from './package-info';
9
+ import { MergedScenario } from './scenario-merger';
10
+ import {
11
+ extractAuthRole,
12
+ extractPriority,
13
+ extractTestcaseType,
14
+ generateTcId,
15
+ mapVpToCategory2,
16
+ splitVpAndName,
17
+ } from './feature-parser';
18
+ import { formatNumberedSteps, formatPrecondition, cleanStepLine } from './step-formatter';
19
+ import { formatTestData } from './test-data-resolver';
20
+ import {
21
+ formatExecutedDate,
22
+ formatNote,
23
+ statusToTestResult,
24
+ } from './playwright-report-parser';
25
+ import { EnvironmentInfo, PlaywrightResult, ScreenSummary, TestCaseRow } from './types';
26
+
27
+ export interface BuildCsvInput {
28
+ screen: string;
29
+ featureName: string;
30
+ merged: MergedScenario[];
31
+ testData: Record<string, string>;
32
+ results: Map<string, PlaywrightResult> | null;
33
+ env: EnvironmentInfo;
34
+ }
35
+
36
+ /**
37
+ * Build CSV test case rows from merged scenarios.
38
+ */
39
+ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
40
+ const rows: TestCaseRow[] = [];
41
+ let fallbackIndex = 1;
42
+
43
+ for (const m of input.merged) {
44
+ const { vpId, category1 } = splitVpAndName(m.feature.name);
45
+ const tcId = generateTcId(input.screen, vpId, fallbackIndex);
46
+ if (!vpId) fallbackIndex++;
47
+
48
+ const category2 = mapVpToCategory2(vpId);
49
+ const priority = extractPriority(m.feature.tags);
50
+ const testcaseType = extractTestcaseType(m.feature.tags);
51
+ const authRole = extractAuthRole(m.feature.tags);
52
+
53
+ // Prefer .spec.ts resolved comments for Steps/Expected if available
54
+ let steps: string;
55
+ let expectedResults: string;
56
+ let precondition: string;
57
+
58
+ if (m.spec) {
59
+ precondition = formatPrecondition(authRole, m.spec.precondition);
60
+ steps = formatNumberedSteps(m.spec.steps);
61
+ expectedResults = formatNumberedSteps(m.spec.expectations);
62
+ } else {
63
+ // Fallback to .feature raw steps
64
+ precondition = formatPrecondition(authRole, m.feature.rawGivenSteps);
65
+ steps = formatNumberedSteps(m.feature.rawWhenSteps);
66
+ expectedResults = formatNumberedSteps(m.feature.rawThenSteps);
67
+ }
68
+
69
+ const testData = formatTestData(m.feature.referencedVars, input.testData);
70
+
71
+ // Match Playwright result by test title (from .spec.ts) OR by scenarioName
72
+ let result: PlaywrightResult | undefined;
73
+ if (input.results && m.spec) {
74
+ result = input.results.get(m.spec.testTitle);
75
+ }
76
+
77
+ // Determine Test Result
78
+ let testResult: string;
79
+ let executedDate = '';
80
+ let note = '';
81
+ let environment = '';
82
+ let executor = '';
83
+
84
+ if (!m.spec) {
85
+ // Scenario not compiled → Manual if @manual, else Not compiled
86
+ testResult = testcaseType === 'Manual' ? 'Pending' : 'N/A';
87
+ if (testcaseType !== 'Manual') {
88
+ note = 'Scenario not compiled — re-run `sungen generate --screen ' + input.screen + '`';
89
+ }
90
+ } else if (!result) {
91
+ // Compiled but no execution record → Pending
92
+ testResult = 'Pending';
93
+ } else {
94
+ testResult = statusToTestResult(result.status);
95
+ executedDate = formatExecutedDate(result.startTime);
96
+ note = formatNote(result);
97
+ environment = `${input.env.baseURL} (${input.env.projectName})`;
98
+ executor = input.env.executor;
99
+ }
100
+
101
+ // If we have environment info and the test actually ran, populate env + executor
102
+ if (testResult !== 'Pending' && testResult !== 'N/A') {
103
+ if (!environment) environment = `${input.env.baseURL} (${input.env.projectName})`;
104
+ if (!executor) executor = input.env.executor;
105
+ }
106
+
107
+ rows.push({
108
+ tcId,
109
+ category1,
110
+ category2,
111
+ category3: input.featureName,
112
+ category4: input.screen,
113
+ precondition,
114
+ testData,
115
+ steps,
116
+ expectedResults,
117
+ priority,
118
+ testcaseType: m.spec ? testcaseType : testcaseType === 'Manual' ? 'Manual' : 'Not compiled',
119
+ testResult,
120
+ executedDate,
121
+ testExecutor: executor,
122
+ testEnvironment: environment,
123
+ note,
124
+ });
125
+ }
126
+
127
+ return rows;
128
+ }
129
+
130
+ /**
131
+ * Calculate summary statistics from rows.
132
+ */
133
+ export function buildSummary(screen: string, rows: TestCaseRow[], outputFile: string): ScreenSummary {
134
+ const summary: ScreenSummary = {
135
+ screen,
136
+ total: rows.length,
137
+ passed: 0,
138
+ failed: 0,
139
+ pending: 0,
140
+ na: 0,
141
+ notCompiled: 0,
142
+ outputFile,
143
+ };
144
+ for (const r of rows) {
145
+ if (r.testResult === 'Passed') summary.passed++;
146
+ else if (r.testResult === 'Failed') summary.failed++;
147
+ else if (r.testResult === 'Pending') summary.pending++;
148
+ else if (r.testResult === 'N/A') summary.na++;
149
+ if (r.testcaseType === 'Not compiled') summary.notCompiled++;
150
+ }
151
+ return summary;
152
+ }
153
+
154
+ // ----------------------------------------------------------------------------
155
+ // CSV writer
156
+ // ----------------------------------------------------------------------------
157
+
158
+ /**
159
+ * Escape a CSV cell per RFC 4180.
160
+ * Wrap in quotes if contains: comma, newline, quote. Escape embedded " as "".
161
+ */
162
+ function csvCell(v: string | number | undefined | null): string {
163
+ if (v === null || v === undefined) return '';
164
+ const s = String(v);
165
+ if (/[",\n\r]/.test(s)) {
166
+ return '"' + s.replace(/"/g, '""') + '"';
167
+ }
168
+ return s;
169
+ }
170
+
171
+ function csvRow(cells: Array<string | number | undefined | null>): string {
172
+ return cells.map(csvCell).join(',');
173
+ }
174
+
175
+ /**
176
+ * Compose the full CSV file content matching the BM-2-901-13 template.
177
+ */
178
+ export function renderCsv(summary: ScreenSummary, rows: TestCaseRow[], specLink: string): string {
179
+ const issueDate = (() => {
180
+ const d = new Date();
181
+ return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
182
+ })();
183
+
184
+ const total = summary.total || 1; // avoid div by zero
185
+ const pct = (n: number) => `${Math.round((n / total) * 100)}%`;
186
+
187
+ // UTF-8 BOM for Excel compatibility with Vietnamese
188
+ const BOM = '\ufeff';
189
+ const lines: string[] = [];
190
+
191
+ // Header metadata block (mirrors sample)
192
+ const titleLabel = `${summary.screen.toUpperCase()} TESTCASE`;
193
+ lines.push(csvRow(['', '', '', titleLabel, '', '', 'No: BM-2-901-13', '', '', '', '', '', '', '', '', '']));
194
+ lines.push(csvRow(['', '', '', '', '', '', `Version: ${getPackageVersion()}`, '', '', '', '', '', '', '', '', '']));
195
+ lines.push(csvRow(['', '', '', '', '', '', `Issue Date: ${issueDate}`, '', '', '', '', '', '', '', '', '']));
196
+ lines.push(csvRow(['SUN ASTERISK VIETNAM CO., LTD', '', '', '', '', '', 'ISO/IEC 27001:2022 & ISO 9001:2015', '', '', '', '', '', '', '', '', '']));
197
+ lines.push(csvRow(['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']));
198
+ lines.push(csvRow(['', '', 'Total TCs', 'Passed', 'Failed', 'Pending', 'N/A', 'Remaining', '', '', '', '', '', '', '', '']));
199
+ lines.push(csvRow(['', '', summary.total, summary.passed, summary.failed, summary.pending, summary.na, summary.pending + summary.na, '', '', '', '', '', '', '', '']));
200
+ lines.push(csvRow(['', '', '', pct(summary.passed), pct(summary.failed), pct(summary.pending), pct(summary.na), pct(summary.pending + summary.na), '', '', '', '', '', '', '', '']));
201
+ lines.push(csvRow(['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']));
202
+ lines.push(csvRow(['Spec/Design link:', specLink, '', '', '', '', '', '', '', '', '', '', '', '', '', '']));
203
+ lines.push(csvRow(['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']));
204
+ lines.push(csvRow(['*: Mandatory', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']));
205
+ lines.push(csvRow([
206
+ 'TC ID*',
207
+ '{Category 1}',
208
+ '{Category 2}',
209
+ '{Category 3}',
210
+ '{Category 4}',
211
+ 'Pre-condition',
212
+ 'Test Data',
213
+ 'Steps*',
214
+ 'Expected results*',
215
+ 'Priority',
216
+ 'Testcase type',
217
+ 'Test Result*',
218
+ 'Executed Date*',
219
+ 'Test Executor*',
220
+ 'Test Environment',
221
+ 'Note\n(Test evidence, DefectID, Actual result)',
222
+ ]));
223
+
224
+ // Group rows by Category 2 (in stable order)
225
+ const order = ['Accessing', 'GUI', 'Function'];
226
+ const grouped = new Map<string, TestCaseRow[]>();
227
+ for (const row of rows) {
228
+ const g = grouped.get(row.category2) || [];
229
+ g.push(row);
230
+ grouped.set(row.category2, g);
231
+ }
232
+ const emittedGroups = new Set<string>();
233
+ for (const group of order) {
234
+ const groupRows = grouped.get(group);
235
+ if (!groupRows || groupRows.length === 0) continue;
236
+ emittedGroups.add(group);
237
+ // Section header row (empty Category 1, group name in Category 2 column? sample puts it differently — empty row then category marker)
238
+ lines.push(csvRow(['', group, '', '', '', '', '', '', '', '', '', '', '', '', '', '']));
239
+ for (const row of groupRows) {
240
+ lines.push(csvRow([
241
+ row.tcId,
242
+ row.category1,
243
+ '',
244
+ '',
245
+ '',
246
+ row.precondition,
247
+ row.testData,
248
+ row.steps,
249
+ row.expectedResults,
250
+ row.priority,
251
+ row.testcaseType,
252
+ row.testResult,
253
+ row.executedDate,
254
+ row.testExecutor,
255
+ row.testEnvironment,
256
+ row.note,
257
+ ]));
258
+ }
259
+ }
260
+ // Emit any groups not in the predefined order
261
+ for (const [group, groupRows] of grouped.entries()) {
262
+ if (emittedGroups.has(group)) continue;
263
+ lines.push(csvRow(['', group, '', '', '', '', '', '', '', '', '', '', '', '', '', '']));
264
+ for (const row of groupRows) {
265
+ lines.push(csvRow([
266
+ row.tcId,
267
+ row.category1,
268
+ '',
269
+ '',
270
+ '',
271
+ row.precondition,
272
+ row.testData,
273
+ row.steps,
274
+ row.expectedResults,
275
+ row.priority,
276
+ row.testcaseType,
277
+ row.testResult,
278
+ row.executedDate,
279
+ row.testExecutor,
280
+ row.testEnvironment,
281
+ row.note,
282
+ ]));
283
+ }
284
+ }
285
+
286
+ return BOM + lines.join('\n') + '\n';
287
+ }
288
+
289
+ /**
290
+ * Write the CSV to disk at qa/deliverables/<screen>-testcases.csv.
291
+ * Creates directory if needed.
292
+ */
293
+ export function writeCsv(cwd: string, screen: string, csvContent: string): string {
294
+ const outDir = path.join(cwd, 'qa', 'deliverables');
295
+ if (!fs.existsSync(outDir)) {
296
+ fs.mkdirSync(outDir, { recursive: true });
297
+ }
298
+ const outPath = path.join(outDir, `${screen}-testcases.csv`);
299
+ fs.writeFileSync(outPath, csvContent, 'utf-8');
300
+ return outPath;
301
+ }
302
+
303
+ // mark unused import to silence TS if needed
304
+ void cleanStepLine;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Parse .feature files to extract scenario metadata for CSV export.
3
+ * Reuses the existing GherkinParser.
4
+ */
5
+
6
+ import { GherkinParser, ParsedFeature, ParsedScenario } from '../generators/gherkin-parser';
7
+ import { FeatureMetadata, ScenarioMetadata } from './types';
8
+
9
+ /**
10
+ * Variables referenced in a scenario: find all {{var_name}} in step text.
11
+ */
12
+ function extractReferencedVars(scenario: ParsedScenario): string[] {
13
+ const vars = new Set<string>();
14
+ for (const step of scenario.steps) {
15
+ const matches = step.text.matchAll(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g);
16
+ for (const match of matches) {
17
+ vars.add(match[1]);
18
+ }
19
+ // Also check inline DataTable cells
20
+ if (step.dataTable) {
21
+ for (const row of step.dataTable.rows) {
22
+ for (const cell of row.cells) {
23
+ const cellMatches = cell.matchAll(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g);
24
+ for (const match of cellMatches) {
25
+ vars.add(match[1]);
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return Array.from(vars);
32
+ }
33
+
34
+ /**
35
+ * Classify each step into Given / When / Then bucket based on its preceding
36
+ * explicit keyword. "And" inherits from the previous explicit keyword.
37
+ */
38
+ function classifySteps(scenario: ParsedScenario): { given: string[]; when: string[]; then: string[] } {
39
+ const given: string[] = [];
40
+ const when: string[] = [];
41
+ const then: string[] = [];
42
+ let currentBucket: 'given' | 'when' | 'then' = 'given';
43
+
44
+ for (const step of scenario.steps) {
45
+ const keyword = step.keyword.trim();
46
+ if (keyword === 'Given') currentBucket = 'given';
47
+ else if (keyword === 'When') currentBucket = 'when';
48
+ else if (keyword === 'Then') currentBucket = 'then';
49
+ // And / But → inherit currentBucket
50
+
51
+ if (currentBucket === 'given') given.push(step.text);
52
+ else if (currentBucket === 'when') when.push(step.text);
53
+ else then.push(step.text);
54
+ }
55
+
56
+ return { given, when, then };
57
+ }
58
+
59
+ /**
60
+ * Parse .feature file → structured metadata ready for CSV assembly.
61
+ */
62
+ export function parseFeatureMetadata(featureFilePath: string): FeatureMetadata {
63
+ const parser = new GherkinParser();
64
+ const parsed: ParsedFeature = parser.parseFeatureFile(featureFilePath);
65
+
66
+ const scenarios: ScenarioMetadata[] = parsed.scenarios.map((sc) => {
67
+ const { given, when, then } = classifySteps(sc);
68
+
69
+ return {
70
+ name: sc.name,
71
+ tags: [...parsed.tags, ...sc.tags],
72
+ stepsName: sc.stepsName,
73
+ extendsName: sc.extendsName,
74
+ referencedVars: extractReferencedVars(sc),
75
+ rawGivenSteps: given,
76
+ rawWhenSteps: when,
77
+ rawThenSteps: then,
78
+ };
79
+ });
80
+
81
+ return {
82
+ featureName: parsed.name,
83
+ featurePath: parsed.path,
84
+ featureTags: parsed.tags,
85
+ scenarios,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Return true if this scenario is a @steps:<name> base scenario that should
91
+ * be excluded from the CSV (it's only used for @extend inheritance).
92
+ */
93
+ export function isStepsBaseScenario(sc: ScenarioMetadata): boolean {
94
+ return !!sc.stepsName;
95
+ }
96
+
97
+ /**
98
+ * Return true if this is the default scaffold sample scenario (not real).
99
+ */
100
+ export function isSampleScaffoldScenario(sc: ScenarioMetadata): boolean {
101
+ return /^Sample scenario for /i.test(sc.name);
102
+ }
103
+
104
+ /**
105
+ * Extract priority from scenario tags.
106
+ */
107
+ export function extractPriority(tags: string[]): string {
108
+ if (tags.includes('@critical')) return 'Critical';
109
+ if (tags.includes('@high')) return 'High';
110
+ if (tags.includes('@normal')) return 'Normal';
111
+ if (tags.includes('@low')) return 'Low';
112
+ return 'Normal';
113
+ }
114
+
115
+ /**
116
+ * Extract testcase type from scenario tags.
117
+ */
118
+ export function extractTestcaseType(tags: string[]): 'Auto' | 'Manual' {
119
+ return tags.includes('@manual') ? 'Manual' : 'Auto';
120
+ }
121
+
122
+ /**
123
+ * Extract auth role from tags. Returns 'no-auth' if @no-auth, else role name or null.
124
+ */
125
+ export function extractAuthRole(tags: string[]): string | null {
126
+ if (tags.includes('@no-auth')) return 'no-auth';
127
+ const authTag = tags.find((t) => t.startsWith('@auth:'));
128
+ if (authTag) return authTag.replace('@auth:', '');
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Extract VP ID and human-readable name from scenario name like
134
+ * "VP-UI-001 Modal displays all required fields" →
135
+ * { vpId: "VP-UI-001", category1: "Modal displays all required fields" }
136
+ */
137
+ export function splitVpAndName(scenarioName: string): { vpId?: string; category1: string } {
138
+ const match = scenarioName.match(/^(VP-[A-Z]+-\d+[a-zA-Z]?)\s+(.+)$/);
139
+ if (match) {
140
+ return { vpId: match[1], category1: match[2] };
141
+ }
142
+ return { category1: scenarioName };
143
+ }
144
+
145
+ /**
146
+ * Map VP prefix to Category 2.
147
+ */
148
+ export function mapVpToCategory2(vpId: string | undefined): string {
149
+ if (!vpId) return 'Function';
150
+ if (vpId.startsWith('VP-SEC-')) return 'Accessing';
151
+ if (vpId.startsWith('VP-UI-')) return 'GUI';
152
+ if (vpId.startsWith('VP-VAL-')) return 'Function';
153
+ if (vpId.startsWith('VP-LOGIC-')) return 'Function';
154
+ return 'Function';
155
+ }
156
+
157
+ /**
158
+ * Generate TC ID: <SCREEN_UPPER>-<VP-part>-<NNN> OR <SCREEN_UPPER>-<NNN> if no VP.
159
+ */
160
+ export function generateTcId(screen: string, vpId: string | undefined, fallbackIndex: number): string {
161
+ const screenUpper = screen.toUpperCase().replace(/[^A-Z0-9]/g, '-');
162
+ if (vpId) {
163
+ // VP-UI-001 → UI-001
164
+ const vpPart = vpId.replace(/^VP-/, '');
165
+ return `${screenUpper}-${vpPart}`;
166
+ }
167
+ return `${screenUpper}-${String(fallbackIndex).padStart(3, '0')}`;
168
+ }
@@ -0,0 +1,35 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ let cached: string | null = null;
5
+
6
+ /**
7
+ * Read `version` from the sungen package.json at module-root so the delivery
8
+ * report always reflects the installed CLI version instead of a hardcoded
9
+ * string. The lookup walks up from __dirname (works under both src/ via ts-node
10
+ * and dist/ after compilation) until a package.json with a `name` starting
11
+ * with `@sun-asterisk/sungen` or `sungen` is found.
12
+ */
13
+ export function getPackageVersion(): string {
14
+ if (cached) return cached;
15
+ let dir = __dirname;
16
+ for (let i = 0; i < 6; i++) {
17
+ const pkgPath = path.join(dir, 'package.json');
18
+ if (fs.existsSync(pkgPath)) {
19
+ try {
20
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
21
+ if (pkg && typeof pkg.version === 'string') {
22
+ cached = pkg.version;
23
+ return cached;
24
+ }
25
+ } catch {
26
+ // ignore and keep walking
27
+ }
28
+ }
29
+ const parent = path.dirname(dir);
30
+ if (parent === dir) break;
31
+ dir = parent;
32
+ }
33
+ cached = 'unknown';
34
+ return cached;
35
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Parse Playwright results.json (JSON reporter output) to map test titles → execution status.
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import { PlaywrightResult } from './types';
7
+
8
+ /**
9
+ * Shape of the JSON reporter output we care about.
10
+ * See https://playwright.dev/docs/test-reporters#json-reporter
11
+ */
12
+ interface PlaywrightJsonReport {
13
+ config?: { projects?: Array<{ name?: string }> };
14
+ suites?: PlaywrightSuite[];
15
+ }
16
+
17
+ interface PlaywrightSuite {
18
+ title?: string;
19
+ suites?: PlaywrightSuite[];
20
+ specs?: PlaywrightSpec[];
21
+ }
22
+
23
+ interface PlaywrightSpec {
24
+ title?: string; // scenario title
25
+ tests?: Array<{
26
+ results?: Array<{
27
+ status?: string; // 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted'
28
+ startTime?: string;
29
+ error?: { message?: string; stack?: string };
30
+ attachments?: Array<{ name?: string; path?: string; contentType?: string }>;
31
+ }>;
32
+ }>;
33
+ }
34
+
35
+ /**
36
+ * Recursively walk suites and collect (full-title → spec) pairs.
37
+ * Title stack is joined with " > " to match our SpecTest.testTitle format.
38
+ */
39
+ function walkSuites(suites: PlaywrightSuite[] | undefined, titleStack: string[]): Map<string, PlaywrightSpec> {
40
+ const map = new Map<string, PlaywrightSpec>();
41
+ if (!suites) return map;
42
+ for (const suite of suites) {
43
+ const nextStack = suite.title ? [...titleStack, suite.title] : titleStack;
44
+ for (const spec of suite.specs || []) {
45
+ if (spec.title) {
46
+ const fullTitle = [...nextStack, spec.title].join(' > ');
47
+ map.set(fullTitle, spec);
48
+ }
49
+ }
50
+ const childMap = walkSuites(suite.suites, nextStack);
51
+ for (const [k, v] of childMap.entries()) map.set(k, v);
52
+ }
53
+ return map;
54
+ }
55
+
56
+ /**
57
+ * Load and parse results.json. Returns null if file missing or invalid.
58
+ */
59
+ export function loadPlaywrightReport(reportPath: string): Map<string, PlaywrightResult> | null {
60
+ if (!fs.existsSync(reportPath)) return null;
61
+ let raw: PlaywrightJsonReport;
62
+ try {
63
+ const content = fs.readFileSync(reportPath, 'utf-8');
64
+ raw = JSON.parse(content);
65
+ } catch {
66
+ return null;
67
+ }
68
+
69
+ // The top-level "suites" array uses the spec file path as suite.title.
70
+ // Tests are nested deeper under describe() blocks.
71
+ // We walk all suites and map "describeTitle > nestedTitle > specTitle" → spec.
72
+ const specMap = walkSuites(raw.suites, []);
73
+
74
+ const result = new Map<string, PlaywrightResult>();
75
+ for (const [fullTitle, spec] of specMap.entries()) {
76
+ const test = spec.tests?.[0];
77
+ const res = test?.results?.[0];
78
+ const status = normalizeStatus(res?.status);
79
+ const errorMsg = res?.error?.message || '';
80
+ const trace = res?.attachments?.find((a) => a.name === 'trace' || a.contentType === 'application/zip')?.path;
81
+
82
+ // Strip the leading file-path suite title if present.
83
+ // The top-level suite title is usually a file path like "specs/generated/kudos/kudos.spec.ts".
84
+ // The test title we match against is "<Feature> > <auth> > <Scenario>".
85
+ const stripped = stripLeadingFilePathSuite(fullTitle);
86
+
87
+ result.set(stripped, {
88
+ testTitle: stripped,
89
+ status,
90
+ startTime: res?.startTime,
91
+ error: errorMsg,
92
+ tracePath: trace,
93
+ });
94
+ }
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * Normalize playwright status strings to our enum.
100
+ */
101
+ function normalizeStatus(s: string | undefined): PlaywrightResult['status'] {
102
+ if (!s) return 'unknown';
103
+ if (s === 'passed') return 'passed';
104
+ if (s === 'failed') return 'failed';
105
+ if (s === 'timedOut') return 'timedOut';
106
+ if (s === 'skipped') return 'skipped';
107
+ if (s === 'interrupted') return 'interrupted';
108
+ return 'unknown';
109
+ }
110
+
111
+ /**
112
+ * If the full title starts with a file-path-looking suite title like
113
+ * "specs/generated/kudos/kudos.spec.ts > Feature Name > ...",
114
+ * strip that leading segment so matching works against "Feature Name > ...".
115
+ */
116
+ function stripLeadingFilePathSuite(fullTitle: string): string {
117
+ const parts = fullTitle.split(' > ');
118
+ if (parts.length > 1 && /\.spec\.ts$/i.test(parts[0])) {
119
+ return parts.slice(1).join(' > ');
120
+ }
121
+ return fullTitle;
122
+ }
123
+
124
+ /**
125
+ * CSV "Test Result" column mapping.
126
+ */
127
+ export function statusToTestResult(status: PlaywrightResult['status']): string {
128
+ switch (status) {
129
+ case 'passed':
130
+ return 'Passed';
131
+ case 'failed':
132
+ case 'timedOut':
133
+ return 'Failed';
134
+ case 'skipped':
135
+ return 'N/A';
136
+ case 'interrupted':
137
+ return 'Pending';
138
+ default:
139
+ return 'Pending';
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Format startTime (ISO string) → "dd/mm/yyyy".
145
+ */
146
+ export function formatExecutedDate(startTime: string | undefined): string {
147
+ if (!startTime) return '';
148
+ const d = new Date(startTime);
149
+ if (isNaN(d.getTime())) return '';
150
+ const dd = String(d.getDate()).padStart(2, '0');
151
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
152
+ const yyyy = d.getFullYear();
153
+ return `${dd}/${mm}/${yyyy}`;
154
+ }
155
+
156
+ /**
157
+ * Format error message for Note column. Truncate long stacks, include trace path.
158
+ */
159
+ export function formatNote(result: PlaywrightResult | undefined): string {
160
+ if (!result) return '';
161
+ if (result.status !== 'failed' && result.status !== 'timedOut') return '';
162
+ const firstLine = (result.error || '').split('\n')[0].trim();
163
+ const truncated = firstLine.length > 200 ? firstLine.substring(0, 197) + '…' : firstLine;
164
+ const parts: string[] = [];
165
+ if (truncated) parts.push(`Error: ${truncated}`);
166
+ if (result.tracePath) parts.push(`Trace: ${result.tracePath}`);
167
+ return parts.join('. ');
168
+ }