@sun-asterisk/sungen 2.4.3 → 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/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +8 -2
- package/dist/cli/commands/add.js.map +1 -1
- 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 +1 -0
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +4 -2
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
- 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 +38 -10
- 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-fix.md +87 -4
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +18 -9
- 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 +45 -20
- 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-fix.md +87 -4
- 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/add.ts +9 -2
- 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 +5 -2
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
- 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 +38 -10
- 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-fix.md +87 -4
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +18 -9
- 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 +45 -20
- 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-fix.md +87 -4
- 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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-validate .feature scenarios against compiled .spec.ts tests.
|
|
3
|
+
* Produces a merged view for CSV row assembly.
|
|
4
|
+
*
|
|
5
|
+
* Priority of truth:
|
|
6
|
+
* - .spec.ts (resolved Playwright calls) is the source of truth for Steps/Expected
|
|
7
|
+
* - .feature (tags, VP ID, referenced vars) is source for metadata
|
|
8
|
+
* - Scenarios in .feature but NOT in .spec.ts → marked "Not compiled"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { FeatureMetadata, ScenarioMetadata, SpecFileData, SpecTest } from './types';
|
|
12
|
+
import { isStepsBaseScenario, isSampleScaffoldScenario } from './feature-parser';
|
|
13
|
+
|
|
14
|
+
export interface MergedScenario {
|
|
15
|
+
/** Scenario metadata from .feature */
|
|
16
|
+
feature: ScenarioMetadata;
|
|
17
|
+
|
|
18
|
+
/** Matched compiled test from .spec.ts, or null if not compiled. */
|
|
19
|
+
spec: SpecTest | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Merge features and spec data, excluding base @steps and scaffold scenarios.
|
|
24
|
+
*/
|
|
25
|
+
export function mergeFeatureAndSpec(
|
|
26
|
+
feature: FeatureMetadata,
|
|
27
|
+
spec: SpecFileData
|
|
28
|
+
): MergedScenario[] {
|
|
29
|
+
const result: MergedScenario[] = [];
|
|
30
|
+
|
|
31
|
+
for (const scenario of feature.scenarios) {
|
|
32
|
+
if (isStepsBaseScenario(scenario)) continue; // skip @steps base scenarios
|
|
33
|
+
if (isSampleScaffoldScenario(scenario)) continue; // skip default sample
|
|
34
|
+
|
|
35
|
+
const specTest = findMatchingSpecTest(scenario, spec.tests);
|
|
36
|
+
result.push({
|
|
37
|
+
feature: scenario,
|
|
38
|
+
spec: specTest,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find the spec test that corresponds to a given feature scenario.
|
|
47
|
+
* Match by scenario title (exact match on the scenario name).
|
|
48
|
+
*/
|
|
49
|
+
function findMatchingSpecTest(scenario: ScenarioMetadata, tests: SpecTest[]): SpecTest | null {
|
|
50
|
+
// Exact title match
|
|
51
|
+
const exact = tests.find((t) => t.scenarioName === scenario.name);
|
|
52
|
+
if (exact) return exact;
|
|
53
|
+
|
|
54
|
+
// Fallback: match by VP-ID prefix (ignore trailing text differences)
|
|
55
|
+
const vpMatch = scenario.name.match(/^(VP-[A-Z]+-\d+[a-zA-Z]?)/);
|
|
56
|
+
if (vpMatch) {
|
|
57
|
+
const vpId = vpMatch[1];
|
|
58
|
+
const byVp = tests.find((t) => t.vpId === vpId);
|
|
59
|
+
if (byVp) return byVp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse compiled .spec.ts files to extract resolved test structure.
|
|
3
|
+
* Uses regex parsing (not TS AST) since files are auto-generated with stable format.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { SpecTest, SpecFileData } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract the inner body of a test('title', async (...) => { BODY }) block.
|
|
11
|
+
* Returns null if no match.
|
|
12
|
+
*/
|
|
13
|
+
function extractTestBlock(content: string, startIdx: number): {
|
|
14
|
+
title: string;
|
|
15
|
+
body: string;
|
|
16
|
+
endIdx: number;
|
|
17
|
+
} | null {
|
|
18
|
+
// Match test('title', async ({ page }) => {
|
|
19
|
+
const testRegex = /test\s*\(\s*['"]([^'"]+)['"]\s*,\s*async\s*\([^)]*\)\s*=>\s*\{/g;
|
|
20
|
+
testRegex.lastIndex = startIdx;
|
|
21
|
+
const match = testRegex.exec(content);
|
|
22
|
+
if (!match) return null;
|
|
23
|
+
|
|
24
|
+
const title = match[1];
|
|
25
|
+
const bodyStart = match.index + match[0].length;
|
|
26
|
+
|
|
27
|
+
// Find matching closing brace (accounting for nested braces)
|
|
28
|
+
let depth = 1;
|
|
29
|
+
let i = bodyStart;
|
|
30
|
+
while (i < content.length && depth > 0) {
|
|
31
|
+
const ch = content[i];
|
|
32
|
+
if (ch === '{') depth++;
|
|
33
|
+
else if (ch === '}') depth--;
|
|
34
|
+
if (depth === 0) break;
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
if (depth !== 0) return null;
|
|
38
|
+
|
|
39
|
+
const body = content.substring(bodyStart, i);
|
|
40
|
+
// The closing `)` of the arrow function call comes after the `}`, e.g., `});` — skip it
|
|
41
|
+
const endIdx = content.indexOf(';', i) + 1;
|
|
42
|
+
return { title, body, endIdx };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find describe('name', ...) blocks and return their names + body ranges.
|
|
47
|
+
*/
|
|
48
|
+
function extractDescribeBlocks(content: string): Array<{ name: string; bodyStart: number; bodyEnd: number }> {
|
|
49
|
+
const result: Array<{ name: string; bodyStart: number; bodyEnd: number }> = [];
|
|
50
|
+
const regex = /test\.describe\s*\(\s*['"]([^'"]+)['"]\s*,\s*\(\s*\)\s*=>\s*\{/g;
|
|
51
|
+
let match: RegExpExecArray | null;
|
|
52
|
+
while ((match = regex.exec(content)) !== null) {
|
|
53
|
+
const name = match[1];
|
|
54
|
+
const bodyStart = match.index + match[0].length;
|
|
55
|
+
let depth = 1;
|
|
56
|
+
let i = bodyStart;
|
|
57
|
+
while (i < content.length && depth > 0) {
|
|
58
|
+
const ch = content[i];
|
|
59
|
+
if (ch === '{') depth++;
|
|
60
|
+
else if (ch === '}') depth--;
|
|
61
|
+
if (depth === 0) break;
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
result.push({ name, bodyStart, bodyEnd: i });
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A "block" in a test body: one leading // comment followed by 1+ await statements.
|
|
71
|
+
*/
|
|
72
|
+
interface CodeBlock {
|
|
73
|
+
comment: string;
|
|
74
|
+
code: string[]; // each string is one logical line (may contain the await keyword)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Tokenize test body into code blocks.
|
|
79
|
+
* Each block = one or more `// <comment>` lines, followed by `await ...` lines until the next comment.
|
|
80
|
+
*/
|
|
81
|
+
function tokenizeBody(body: string): CodeBlock[] {
|
|
82
|
+
const lines = body.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
|
|
83
|
+
const blocks: CodeBlock[] = [];
|
|
84
|
+
let currentComment = '';
|
|
85
|
+
let currentCode: string[] = [];
|
|
86
|
+
|
|
87
|
+
const flush = () => {
|
|
88
|
+
if (currentComment || currentCode.length > 0) {
|
|
89
|
+
blocks.push({ comment: currentComment, code: [...currentCode] });
|
|
90
|
+
}
|
|
91
|
+
currentComment = '';
|
|
92
|
+
currentCode = [];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
if (line.startsWith('//')) {
|
|
97
|
+
// A new comment boundary — flush the previous block first if it has code
|
|
98
|
+
if (currentCode.length > 0) flush();
|
|
99
|
+
currentComment = line.replace(/^\/\/\s*/, '').trim();
|
|
100
|
+
} else if (line.startsWith('await ') || line.startsWith('await\t')) {
|
|
101
|
+
currentCode.push(line);
|
|
102
|
+
} else if (line.endsWith(';') || line.endsWith('{') || line.endsWith('}')) {
|
|
103
|
+
// Continuation of an await (multi-line code) OR block-structure like `{ ... }`
|
|
104
|
+
if (currentCode.length > 0) {
|
|
105
|
+
currentCode[currentCode.length - 1] += ' ' + line;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
flush();
|
|
110
|
+
return blocks;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Classify a CodeBlock into precondition / step / expectation.
|
|
115
|
+
*
|
|
116
|
+
* Rules:
|
|
117
|
+
* - Starts with page.goto or is from @steps setup → precondition
|
|
118
|
+
* - Contains `expect(` → expectation
|
|
119
|
+
* - Otherwise → step (action)
|
|
120
|
+
*
|
|
121
|
+
* Additional heuristic: if the comment matches "Open <screen> page" or similar
|
|
122
|
+
* setup verbs, treat as precondition even if not first.
|
|
123
|
+
*/
|
|
124
|
+
function classifyBlock(block: CodeBlock): 'precondition' | 'step' | 'expectation' {
|
|
125
|
+
const codeJoined = block.code.join(' ');
|
|
126
|
+
if (codeJoined.includes('expect(')) return 'expectation';
|
|
127
|
+
if (codeJoined.includes('page.goto(')) return 'precondition';
|
|
128
|
+
return 'step';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Try to extract VP ID (e.g., VP-UI-001) from the start of a scenario title.
|
|
133
|
+
*/
|
|
134
|
+
function extractVpId(title: string): string | undefined {
|
|
135
|
+
const match = title.match(/^(VP-[A-Z]+-\d+[a-zA-Z]?)\b/);
|
|
136
|
+
return match ? match[1] : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse a compiled .spec.ts file.
|
|
141
|
+
*
|
|
142
|
+
* Structure:
|
|
143
|
+
* test.describe('<Feature Name>', () => {
|
|
144
|
+
* test.describe('<auth role>', () => { // optional nesting
|
|
145
|
+
* test.use({ storageState: '...' });
|
|
146
|
+
* test('<title>', async ({ page }) => { ... });
|
|
147
|
+
* ...
|
|
148
|
+
* });
|
|
149
|
+
* test('<title>', async ({ page }) => { ... }); // no-auth cases at top level
|
|
150
|
+
* });
|
|
151
|
+
*/
|
|
152
|
+
export function parseSpecFile(specFilePath: string): SpecFileData {
|
|
153
|
+
if (!fs.existsSync(specFilePath)) {
|
|
154
|
+
return { tests: [] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const content = fs.readFileSync(specFilePath, 'utf-8');
|
|
158
|
+
const tests: SpecTest[] = [];
|
|
159
|
+
|
|
160
|
+
const describes = extractDescribeBlocks(content);
|
|
161
|
+
if (describes.length === 0) return { tests: [] };
|
|
162
|
+
|
|
163
|
+
// First describe is the feature-level one
|
|
164
|
+
const featureDescribe = describes[0];
|
|
165
|
+
const featureName = featureDescribe.name;
|
|
166
|
+
|
|
167
|
+
// Nested auth describes live inside the feature describe range
|
|
168
|
+
const nestedAuthDescribes = describes.filter(
|
|
169
|
+
(d) => d !== featureDescribe && d.bodyStart > featureDescribe.bodyStart && d.bodyEnd < featureDescribe.bodyEnd
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Collect tests inside each nested auth describe
|
|
173
|
+
const processedRanges: Array<[number, number]> = [];
|
|
174
|
+
for (const authDesc of nestedAuthDescribes) {
|
|
175
|
+
const innerBody = content.substring(authDesc.bodyStart, authDesc.bodyEnd);
|
|
176
|
+
const innerTests = collectTestsFromBody(innerBody, featureName, authDesc.name);
|
|
177
|
+
tests.push(...innerTests);
|
|
178
|
+
processedRanges.push([authDesc.bodyStart, authDesc.bodyEnd]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Collect tests at the feature level that are NOT inside any nested auth describe
|
|
182
|
+
// i.e., top-level tests (no auth / @no-auth scenarios)
|
|
183
|
+
let cursor = featureDescribe.bodyStart;
|
|
184
|
+
const featureBodyEnd = featureDescribe.bodyEnd;
|
|
185
|
+
|
|
186
|
+
while (cursor < featureBodyEnd) {
|
|
187
|
+
// Skip any nested describe range
|
|
188
|
+
const nextSkip = processedRanges.find(([s, e]) => s > cursor && s < featureBodyEnd && e > cursor);
|
|
189
|
+
const searchEnd = nextSkip ? nextSkip[0] : featureBodyEnd;
|
|
190
|
+
|
|
191
|
+
const slice = content.substring(cursor, searchEnd);
|
|
192
|
+
const sliceTests = collectTestsFromBody(slice, featureName, undefined);
|
|
193
|
+
tests.push(...sliceTests);
|
|
194
|
+
|
|
195
|
+
cursor = nextSkip ? nextSkip[1] : featureBodyEnd;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { tests };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collectTestsFromBody(body: string, describeName: string, authRole?: string): SpecTest[] {
|
|
202
|
+
const tests: SpecTest[] = [];
|
|
203
|
+
let cursor = 0;
|
|
204
|
+
while (cursor < body.length) {
|
|
205
|
+
const block = extractTestBlock(body, cursor);
|
|
206
|
+
if (!block) break;
|
|
207
|
+
|
|
208
|
+
const codeBlocks = tokenizeBody(block.body);
|
|
209
|
+
const precondition: string[] = [];
|
|
210
|
+
const steps: string[] = [];
|
|
211
|
+
const expectations: string[] = [];
|
|
212
|
+
|
|
213
|
+
for (const cb of codeBlocks) {
|
|
214
|
+
const category = classifyBlock(cb);
|
|
215
|
+
const line = cb.comment || cb.code.join(' ');
|
|
216
|
+
if (category === 'precondition') precondition.push(line);
|
|
217
|
+
else if (category === 'step') steps.push(line);
|
|
218
|
+
else expectations.push(line);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
tests.push({
|
|
222
|
+
describeName,
|
|
223
|
+
authRole,
|
|
224
|
+
testTitle: buildFullTestTitle(describeName, authRole, block.title),
|
|
225
|
+
scenarioName: block.title,
|
|
226
|
+
vpId: extractVpId(block.title),
|
|
227
|
+
precondition,
|
|
228
|
+
steps,
|
|
229
|
+
expectations,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
cursor = block.endIdx;
|
|
233
|
+
}
|
|
234
|
+
return tests;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build full test title matching Playwright report format:
|
|
239
|
+
* "<Feature Name> > <auth role> > <scenario title>"
|
|
240
|
+
* Playwright separator is " > " in CLI output, but internally it uses title arrays.
|
|
241
|
+
*/
|
|
242
|
+
function buildFullTestTitle(featureName: string, authRole: string | undefined, scenarioTitle: string): string {
|
|
243
|
+
if (authRole) {
|
|
244
|
+
return `${featureName} > ${authRole} > ${scenarioTitle}`;
|
|
245
|
+
}
|
|
246
|
+
return `${featureName} > ${scenarioTitle}`;
|
|
247
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert raw Gherkin step text OR generated .spec.ts comment lines into
|
|
3
|
+
* concise, numbered natural language for the CSV Steps/Expected columns.
|
|
4
|
+
*
|
|
5
|
+
* Deterministic — no AI calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format a list of raw step lines for the Steps / Expected Results columns.
|
|
10
|
+
*
|
|
11
|
+
* - 0 lines → empty string
|
|
12
|
+
* - 1 line → the line verbatim (no "1. " prefix — avoids Google Sheets clutter)
|
|
13
|
+
* - ≥ 2 → each line on its own row, "1. ", "2. ", … prefixes, newline-joined
|
|
14
|
+
* so spreadsheet cells auto-wrap one step per visual line.
|
|
15
|
+
*/
|
|
16
|
+
export function formatNumberedSteps(lines: string[]): string {
|
|
17
|
+
const cleaned = lines.map(cleanStepLine).filter((l) => l.length > 0);
|
|
18
|
+
if (cleaned.length === 0) return '';
|
|
19
|
+
if (cleaned.length === 1) return cleaned[0];
|
|
20
|
+
return cleaned.map((line, idx) => `${idx + 1}. ${line}`).join('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clean a raw step line (either Gherkin text or .spec.ts comment).
|
|
25
|
+
*
|
|
26
|
+
* Handles patterns like:
|
|
27
|
+
* "User click [login] button" → "Click 'login' button"
|
|
28
|
+
* "User fill [Email] field with {{email}}" → "Fill 'Email' field with {{email}}"
|
|
29
|
+
* "Click Signature 2025 Creator" → "Click 'Signature 2025 Creator'"
|
|
30
|
+
* "Assert title error has text err_xxx" → "Title error has text err_xxx"
|
|
31
|
+
* "Open awards page" → "Open awards page"
|
|
32
|
+
*/
|
|
33
|
+
export function cleanStepLine(raw: string): string {
|
|
34
|
+
let s = raw.trim();
|
|
35
|
+
|
|
36
|
+
// Strip leading "User " actor
|
|
37
|
+
s = s.replace(/^User\s+/i, '');
|
|
38
|
+
|
|
39
|
+
// Strip Gherkin-style element decoration "[Ref] type" → "'Ref' type"
|
|
40
|
+
// e.g., "click [Login] button" → "click 'Login' button"
|
|
41
|
+
s = s.replace(/\[([^\]]+)\]/g, "'$1'");
|
|
42
|
+
|
|
43
|
+
// Collapse multiple spaces
|
|
44
|
+
s = s.replace(/\s+/g, ' ');
|
|
45
|
+
|
|
46
|
+
// For .spec.ts comments that start with "Assert ..." keep as-is (they read naturally)
|
|
47
|
+
// For action comments like "Click X", "Fill X with Y" keep as-is.
|
|
48
|
+
|
|
49
|
+
// Capitalize first letter
|
|
50
|
+
if (s.length > 0) s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
51
|
+
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Natural language for Given → Pre-condition.
|
|
57
|
+
* Combines auth info + Given steps into a single sentence.
|
|
58
|
+
*/
|
|
59
|
+
export function formatPrecondition(
|
|
60
|
+
authRole: string | null,
|
|
61
|
+
givenLines: string[]
|
|
62
|
+
): string {
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
|
|
65
|
+
// Auth prefix
|
|
66
|
+
if (authRole === 'no-auth') {
|
|
67
|
+
parts.push('Not authenticated.');
|
|
68
|
+
} else if (authRole) {
|
|
69
|
+
parts.push(`Logged in as ${authRole}.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Given lines (clean + period-separated)
|
|
73
|
+
for (const line of givenLines) {
|
|
74
|
+
const cleaned = cleanStepLine(line);
|
|
75
|
+
if (!cleaned) continue;
|
|
76
|
+
parts.push(cleaned.endsWith('.') ? cleaned : cleaned + '.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parts.join(' ').trim();
|
|
80
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve test-data.yaml variables for the CSV Test Data column.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { parse as parseYaml } from 'yaml';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load test-data.yaml into a flat key→value map.
|
|
11
|
+
* Returns empty map if file missing.
|
|
12
|
+
*/
|
|
13
|
+
export function loadTestData(testDataFilePath: string): Record<string, string> {
|
|
14
|
+
if (!fs.existsSync(testDataFilePath)) return {};
|
|
15
|
+
const content = fs.readFileSync(testDataFilePath, 'utf-8');
|
|
16
|
+
const parsed = parseYaml(content);
|
|
17
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
18
|
+
|
|
19
|
+
// Flatten shallow object to string values
|
|
20
|
+
const result: Record<string, string> = {};
|
|
21
|
+
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
22
|
+
if (value === null || value === undefined) continue;
|
|
23
|
+
if (typeof value === 'object') continue; // skip nested structures
|
|
24
|
+
result[key] = String(value);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format a list of variable references into a "key: value; key2: value2" string
|
|
31
|
+
* suitable for the CSV Test Data column.
|
|
32
|
+
*
|
|
33
|
+
* Long values (>80 chars) are truncated with "…".
|
|
34
|
+
*/
|
|
35
|
+
export function formatTestData(
|
|
36
|
+
referencedVars: string[],
|
|
37
|
+
testData: Record<string, string>
|
|
38
|
+
): string {
|
|
39
|
+
if (referencedVars.length === 0) return '';
|
|
40
|
+
const pairs: string[] = [];
|
|
41
|
+
for (const key of referencedVars) {
|
|
42
|
+
const value = testData[key];
|
|
43
|
+
if (value === undefined) continue;
|
|
44
|
+
pairs.push(`${key}: ${truncate(value, 80)}`);
|
|
45
|
+
}
|
|
46
|
+
return pairs.join('; ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function truncate(s: string, max: number): string {
|
|
50
|
+
if (s.length <= max) return s;
|
|
51
|
+
return s.substring(0, max - 1) + '…';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the test-data file path for a given screen.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveTestDataPath(cwd: string, screen: string): string {
|
|
58
|
+
return path.join(cwd, 'qa', 'screens', screen, 'test-data', `${screen}.yaml`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the delivery exporter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test case row in the final CSV output.
|
|
7
|
+
*/
|
|
8
|
+
export interface TestCaseRow {
|
|
9
|
+
tcId: string; // e.g. KUDOS-UI-001
|
|
10
|
+
category1: string; // Scenario name (human part)
|
|
11
|
+
category2: string; // "Accessing" | "GUI" | "Function"
|
|
12
|
+
category3: string; // Feature name (e.g. "Create Kudo Modal")
|
|
13
|
+
category4: string; // Screen name (e.g. "kudos")
|
|
14
|
+
precondition: string; // Natural language Given + auth
|
|
15
|
+
testData: string; // semicolon-separated key: value
|
|
16
|
+
steps: string; // numbered steps
|
|
17
|
+
expectedResults: string; // numbered expected results
|
|
18
|
+
priority: string; // Critical | High | Normal | Low
|
|
19
|
+
testcaseType: string; // Auto | Manual | Not compiled
|
|
20
|
+
testResult: string; // Passed | Failed | Pending | N/A
|
|
21
|
+
executedDate: string; // dd/mm/yyyy
|
|
22
|
+
testExecutor: string; // git user.name
|
|
23
|
+
testEnvironment: string; // baseURL + project
|
|
24
|
+
note: string; // error + trace for failed
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Summary stats per screen.
|
|
29
|
+
*/
|
|
30
|
+
export interface ScreenSummary {
|
|
31
|
+
screen: string;
|
|
32
|
+
total: number;
|
|
33
|
+
passed: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
pending: number;
|
|
36
|
+
na: number;
|
|
37
|
+
notCompiled: number;
|
|
38
|
+
outputFile: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pre-flight check result for a single screen.
|
|
43
|
+
*/
|
|
44
|
+
export interface PreflightCheck {
|
|
45
|
+
screen: string;
|
|
46
|
+
featureOk: boolean;
|
|
47
|
+
testDataOk: boolean;
|
|
48
|
+
selectorsOk: boolean;
|
|
49
|
+
specOk: boolean;
|
|
50
|
+
resultsOk: boolean;
|
|
51
|
+
missing: string[]; // Human-readable list of missing items
|
|
52
|
+
suggestions: string[]; // Suggested commands to run
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Metadata extracted from a .feature file.
|
|
57
|
+
*/
|
|
58
|
+
export interface FeatureMetadata {
|
|
59
|
+
featureName: string; // e.g., "Create Kudo Modal"
|
|
60
|
+
featurePath?: string; // e.g., "/kudos"
|
|
61
|
+
featureTags: string[]; // Feature-level tags
|
|
62
|
+
scenarios: ScenarioMetadata[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ScenarioMetadata {
|
|
66
|
+
name: string; // Full scenario name (with VP prefix)
|
|
67
|
+
tags: string[]; // Scenario-level tags (merged with feature tags)
|
|
68
|
+
stepsName?: string; // @steps:<name>
|
|
69
|
+
extendsName?: string; // @extend:<name>
|
|
70
|
+
referencedVars: string[]; // Variables {{var}} used in this scenario
|
|
71
|
+
rawGivenSteps: string[]; // Raw Given/And-after-Given step text
|
|
72
|
+
rawWhenSteps: string[]; // Raw When/And-after-When step text
|
|
73
|
+
rawThenSteps: string[]; // Raw Then/And-after-Then step text
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Structure extracted from a compiled .spec.ts test block.
|
|
78
|
+
*/
|
|
79
|
+
export interface SpecTest {
|
|
80
|
+
describeName: string; // test.describe title
|
|
81
|
+
authRole?: string; // nested describe for auth (e.g., "user")
|
|
82
|
+
testTitle: string; // Full Playwright test title (matches results.json)
|
|
83
|
+
scenarioName: string; // Scenario name without VP prefix stripped
|
|
84
|
+
vpId?: string; // Extracted VP-XX-NNN if present
|
|
85
|
+
precondition: string[]; // Lines before first // [VP-...] marker
|
|
86
|
+
steps: string[]; // Lines between marker and first expect()
|
|
87
|
+
expectations: string[]; // Lines with expect() calls
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SpecFileData {
|
|
91
|
+
tests: SpecTest[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Result from results.json per test.
|
|
96
|
+
*/
|
|
97
|
+
export interface PlaywrightResult {
|
|
98
|
+
testTitle: string;
|
|
99
|
+
status: 'passed' | 'failed' | 'skipped' | 'timedOut' | 'interrupted' | 'unknown';
|
|
100
|
+
startTime?: string;
|
|
101
|
+
error?: string;
|
|
102
|
+
tracePath?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Environment metadata.
|
|
107
|
+
*/
|
|
108
|
+
export interface EnvironmentInfo {
|
|
109
|
+
baseURL: string;
|
|
110
|
+
projectName: string;
|
|
111
|
+
executor: string;
|
|
112
|
+
}
|