@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.
- package/dist/cli/commands/delivery.d.ts +7 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -0
- package/dist/cli/commands/delivery.js +348 -0
- package/dist/cli/commands/delivery.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +64 -1
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +4 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +32 -0
- package/dist/exporters/csv-exporter.d.ts.map +1 -0
- package/dist/exporters/csv-exporter.js +311 -0
- package/dist/exporters/csv-exporter.js.map +1 -0
- package/dist/exporters/feature-parser.d.ts +48 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -0
- package/dist/exporters/feature-parser.js +178 -0
- package/dist/exporters/feature-parser.js.map +1 -0
- package/dist/exporters/package-info.d.ts +9 -0
- package/dist/exporters/package-info.d.ts.map +1 -0
- package/dist/exporters/package-info.js +73 -0
- package/dist/exporters/package-info.js.map +1 -0
- package/dist/exporters/playwright-report-parser.d.ts +21 -0
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -0
- package/dist/exporters/playwright-report-parser.js +184 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts +21 -0
- package/dist/exporters/scenario-merger.d.ts.map +1 -0
- package/dist/exporters/scenario-merger.js +51 -0
- package/dist/exporters/scenario-merger.js.map +1 -0
- package/dist/exporters/spec-parser.d.ts +20 -0
- package/dist/exporters/spec-parser.d.ts.map +1 -0
- package/dist/exporters/spec-parser.js +259 -0
- package/dist/exporters/spec-parser.js.map +1 -0
- package/dist/exporters/step-formatter.d.ts +32 -0
- package/dist/exporters/step-formatter.d.ts.map +1 -0
- package/dist/exporters/step-formatter.js +76 -0
- package/dist/exporters/step-formatter.js.map +1 -0
- package/dist/exporters/test-data-resolver.d.ts +20 -0
- package/dist/exporters/test-data-resolver.d.ts.map +1 -0
- package/dist/exporters/test-data-resolver.js +96 -0
- package/dist/exporters/test-data-resolver.js.map +1 -0
- package/dist/exporters/types.d.ts +104 -0
- package/dist/exporters/types.d.ts.map +1 -0
- package/dist/exporters/types.js +6 -0
- package/dist/exporters/types.js.map +1 -0
- package/dist/exporters/xlsx-exporter.d.ts +19 -0
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -0
- package/dist/exporters/xlsx-exporter.js +309 -0
- package/dist/exporters/xlsx-exporter.js.map +1 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +26 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +12 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +12 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +84 -64
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +2 -0
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +15 -17
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +7 -5
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +71 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +27 -0
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +12 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +142 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +100 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +73 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +103 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -15
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -4
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +71 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -14
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +142 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +100 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +73 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +103 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +22 -0
- package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
- package/dist/orchestrator/templates/playwright.config.js +6 -1
- package/dist/orchestrator/templates/playwright.config.js.map +1 -1
- package/dist/orchestrator/templates/playwright.config.ts +6 -1
- package/package.json +2 -1
- package/src/cli/commands/delivery.ts +348 -0
- package/src/cli/commands/update.ts +84 -2
- package/src/cli/index.ts +4 -2
- package/src/exporters/csv-exporter.ts +304 -0
- package/src/exporters/feature-parser.ts +168 -0
- package/src/exporters/package-info.ts +35 -0
- package/src/exporters/playwright-report-parser.ts +168 -0
- package/src/exporters/scenario-merger.ts +63 -0
- package/src/exporters/spec-parser.ts +247 -0
- package/src/exporters/step-formatter.ts +80 -0
- package/src/exporters/test-data-resolver.ts +59 -0
- package/src/exporters/types.ts +112 -0
- package/src/exporters/xlsx-exporter.ts +301 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +26 -0
- package/src/orchestrator/ai-rules-updater.ts +12 -0
- package/src/orchestrator/project-initializer.ts +103 -70
- package/src/orchestrator/screen-manager.ts +2 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +15 -17
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +7 -5
- package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +71 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +27 -0
- package/src/orchestrator/templates/ai-instructions/claude-config.md +12 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +142 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +100 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +73 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +103 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +2 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -15
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -4
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +71 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -14
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +12 -2
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +142 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +100 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +73 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +103 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +2 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +22 -0
- 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
|
+
}
|