@sun-asterisk/sungen 2.6.12 → 2.6.15
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.map +1 -1
- package/dist/cli/commands/delivery.js +215 -65
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +173 -32
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +84 -84
- package/dist/dashboard/types.d.ts +35 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +24 -3
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +28 -7
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts +15 -0
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +7 -2
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +7 -0
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +20 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/selector-key-resolver.d.ts +55 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -0
- package/dist/exporters/selector-key-resolver.js +208 -0
- package/dist/exporters/selector-key-resolver.js.map +1 -0
- package/dist/exporters/test-data-resolver.d.ts +15 -2
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +61 -8
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +1 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +28 -3
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +34 -6
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +13 -0
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +4 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +48 -14
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +22 -11
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-locale.md +71 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +23 -8
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +45 -6
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-locale.md +316 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -13
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +20 -9
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +70 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +23 -8
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +44 -6
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +291 -0
- package/dist/orchestrator/templates/playwright.config.ts +25 -8
- package/dist/orchestrator/templates/specs-base.ts +9 -0
- package/dist/orchestrator/templates/specs-locale-fixture.ts +105 -0
- package/package.json +1 -1
- package/src/cli/commands/delivery.ts +256 -65
- package/src/cli/index.ts +1 -1
- package/src/dashboard/snapshot-builder.ts +207 -32
- package/src/dashboard/templates/index.html +84 -84
- package/src/dashboard/types.ts +40 -3
- package/src/exporters/csv-exporter.ts +36 -7
- package/src/exporters/json-exporter.ts +22 -2
- package/src/exporters/playwright-report-parser.ts +20 -0
- package/src/exporters/selector-key-resolver.ts +190 -0
- package/src/exporters/test-data-resolver.ts +65 -7
- package/src/exporters/types.ts +1 -0
- package/src/exporters/xlsx-exporter.ts +61 -7
- package/src/generators/test-generator/code-generator.ts +14 -0
- package/src/orchestrator/ai-rules-updater.ts +4 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +48 -14
- package/src/orchestrator/templates/ai-instructions/claude-cmd-dashboard.md +4 -1
- package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +22 -11
- package/src/orchestrator/templates/ai-instructions/claude-cmd-locale.md +71 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +23 -8
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +45 -6
- package/src/orchestrator/templates/ai-instructions/claude-config.md +4 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-locale.md +316 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -13
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-dashboard.md +4 -1
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +20 -9
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +70 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +23 -8
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +44 -6
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +4 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +291 -0
- package/src/orchestrator/templates/playwright.config.ts +25 -8
- package/src/orchestrator/templates/specs-base.ts +9 -0
- package/src/orchestrator/templates/specs-locale-fixture.ts +105 -0
- package/dist/orchestrator/templates/playwright.config.d.ts +0 -10
- package/dist/orchestrator/templates/playwright.config.d.ts.map +0 -1
- package/dist/orchestrator/templates/playwright.config.js +0 -104
- package/dist/orchestrator/templates/playwright.config.js.map +0 -1
- package/dist/orchestrator/templates/specs-base.d.ts +0 -14
- package/dist/orchestrator/templates/specs-base.d.ts.map +0 -1
- package/dist/orchestrator/templates/specs-base.js +0 -77
- package/dist/orchestrator/templates/specs-base.js.map +0 -1
- package/dist/orchestrator/templates/specs-test-data.d.ts +0 -16
- package/dist/orchestrator/templates/specs-test-data.d.ts.map +0 -1
- package/dist/orchestrator/templates/specs-test-data.js +0 -151
- package/dist/orchestrator/templates/specs-test-data.js.map +0 -1
|
@@ -11,6 +11,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
11
11
|
import { parseFeatureMetadata } from '../../exporters/feature-parser';
|
|
12
12
|
import { parseSpecFile } from '../../exporters/spec-parser';
|
|
13
13
|
import { loadTestData } from '../../exporters/test-data-resolver';
|
|
14
|
+
import { loadSelectorKeyMap } from '../../exporters/selector-key-resolver';
|
|
14
15
|
import { loadPlaywrightReport } from '../../exporters/playwright-report-parser';
|
|
15
16
|
import { mergeFeatureAndSpec } from '../../exporters/scenario-merger';
|
|
16
17
|
import {
|
|
@@ -19,8 +20,8 @@ import {
|
|
|
19
20
|
renderCsv,
|
|
20
21
|
writeCsv,
|
|
21
22
|
} from '../../exporters/csv-exporter';
|
|
22
|
-
import { renderXlsx, writeXlsx } from '../../exporters/xlsx-exporter';
|
|
23
|
-
import { EnvironmentInfo, PreflightCheck, ScreenSummary } from '../../exporters/types';
|
|
23
|
+
import { renderXlsx, renderXlsxMultiSheet, writeXlsx } from '../../exporters/xlsx-exporter';
|
|
24
|
+
import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow } from '../../exporters/types';
|
|
24
25
|
|
|
25
26
|
const COLOR = {
|
|
26
27
|
reset: '\x1b[0m',
|
|
@@ -40,46 +41,95 @@ function log(msg: string): void {
|
|
|
40
41
|
// Discovery
|
|
41
42
|
// ----------------------------------------------------------------------------
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A single feature file inside a screen/flow. One DeliveryTarget = one CSV/XLSX.
|
|
46
|
+
*
|
|
47
|
+
* `screen` is the parent directory name under qa/screens or qa/flows.
|
|
48
|
+
* `featureBaseName` is the .feature file basename without extension — typically
|
|
49
|
+
* either `<screen>` (the main feature) or `<screen>-<sub>` (sub-features like
|
|
50
|
+
* `home-modal`). All co-located YAML/spec/result files share this basename.
|
|
51
|
+
*
|
|
52
|
+
* `name` is kept as an alias of `featureBaseName` so existing callers/labels
|
|
53
|
+
* (preflight table, summary) read naturally — every visible row is per-feature.
|
|
54
|
+
*/
|
|
43
55
|
interface DeliveryTarget {
|
|
56
|
+
screen: string;
|
|
57
|
+
featureBaseName: string;
|
|
58
|
+
/** Alias of `featureBaseName` — preserves the old `target.name` call sites. */
|
|
44
59
|
name: string;
|
|
45
60
|
isFlow: boolean;
|
|
46
61
|
}
|
|
47
62
|
|
|
63
|
+
function makeTarget(screen: string, featureBaseName: string, isFlow: boolean): DeliveryTarget {
|
|
64
|
+
return { screen, featureBaseName, name: featureBaseName, isFlow };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List all `.feature` files inside a screen/flow as separate targets.
|
|
69
|
+
* Returns empty array when the directory has no features yet.
|
|
70
|
+
*/
|
|
71
|
+
function listFeatureTargets(cwd: string, screen: string, isFlow: boolean): DeliveryTarget[] {
|
|
72
|
+
const featuresDir = path.join(cwd, 'qa', isFlow ? 'flows' : 'screens', screen, 'features');
|
|
73
|
+
if (!fs.existsSync(featuresDir)) return [];
|
|
74
|
+
return fs.readdirSync(featuresDir)
|
|
75
|
+
.filter((f) => f.endsWith('.feature'))
|
|
76
|
+
.map((f) => makeTarget(screen, f.slice(0, -'.feature'.length), isFlow))
|
|
77
|
+
.sort((a, b) => a.featureBaseName.localeCompare(b.featureBaseName));
|
|
78
|
+
}
|
|
79
|
+
|
|
48
80
|
function listAllTargets(cwd: string): DeliveryTarget[] {
|
|
49
81
|
const targets: DeliveryTarget[] = [];
|
|
50
82
|
|
|
51
83
|
const screensDir = path.join(cwd, 'qa', 'screens');
|
|
52
84
|
if (fs.existsSync(screensDir)) {
|
|
53
85
|
for (const d of fs.readdirSync(screensDir, { withFileTypes: true })) {
|
|
54
|
-
if (d.isDirectory()) targets.push(
|
|
86
|
+
if (d.isDirectory()) targets.push(...listFeatureTargets(cwd, d.name, false));
|
|
55
87
|
}
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
const flowsDir = path.join(cwd, 'qa', 'flows');
|
|
59
91
|
if (fs.existsSync(flowsDir)) {
|
|
60
92
|
for (const d of fs.readdirSync(flowsDir, { withFileTypes: true })) {
|
|
61
|
-
if (d.isDirectory()) targets.push(
|
|
93
|
+
if (d.isDirectory()) targets.push(...listFeatureTargets(cwd, d.name, true));
|
|
62
94
|
}
|
|
63
95
|
}
|
|
64
96
|
|
|
65
|
-
return targets.sort((a, b) => a.
|
|
97
|
+
return targets.sort((a, b) => a.featureBaseName.localeCompare(b.featureBaseName));
|
|
66
98
|
}
|
|
67
99
|
|
|
68
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a positional CLI argument to one or more delivery targets.
|
|
102
|
+
*
|
|
103
|
+
* The argument can be EITHER:
|
|
104
|
+
* - A screen / flow name (e.g. `home`) → all features inside that screen
|
|
105
|
+
* - A specific feature basename (`home-modal`) → just that feature
|
|
106
|
+
*
|
|
107
|
+
* We detect by checking for a directory match first; if none, we search for a
|
|
108
|
+
* feature file with the basename across all screens & flows.
|
|
109
|
+
*/
|
|
110
|
+
function resolveTargetsFromArg(cwd: string, name: string): DeliveryTarget[] {
|
|
69
111
|
if (fs.existsSync(path.join(cwd, 'qa', 'flows', name))) {
|
|
70
|
-
return
|
|
112
|
+
return listFeatureTargets(cwd, name, true);
|
|
71
113
|
}
|
|
72
|
-
|
|
114
|
+
if (fs.existsSync(path.join(cwd, 'qa', 'screens', name))) {
|
|
115
|
+
return listFeatureTargets(cwd, name, false);
|
|
116
|
+
}
|
|
117
|
+
// Treat as feature basename — find the parent screen/flow that hosts it.
|
|
118
|
+
const candidates = listAllTargets(cwd).filter((t) => t.featureBaseName === name);
|
|
119
|
+
if (candidates.length > 0) return candidates;
|
|
120
|
+
// Fallback: treat as screen name even if directory missing (lets preflight
|
|
121
|
+
// surface the "feature file missing" error with the right path).
|
|
122
|
+
return [makeTarget(name, name, false)];
|
|
73
123
|
}
|
|
74
124
|
|
|
75
125
|
function qaDir(cwd: string, target: DeliveryTarget): string {
|
|
76
|
-
return path.join(cwd, 'qa', target.isFlow ? 'flows' : 'screens', target.
|
|
126
|
+
return path.join(cwd, 'qa', target.isFlow ? 'flows' : 'screens', target.screen);
|
|
77
127
|
}
|
|
78
128
|
|
|
79
129
|
function generatedDir(cwd: string, target: DeliveryTarget): string {
|
|
80
130
|
return target.isFlow
|
|
81
|
-
? path.join(cwd, 'specs', 'generated', 'flows', target.
|
|
82
|
-
: path.join(cwd, 'specs', 'generated', target.
|
|
131
|
+
? path.join(cwd, 'specs', 'generated', 'flows', target.screen)
|
|
132
|
+
: path.join(cwd, 'specs', 'generated', target.screen);
|
|
83
133
|
}
|
|
84
134
|
|
|
85
135
|
// ----------------------------------------------------------------------------
|
|
@@ -88,19 +138,96 @@ function generatedDir(cwd: string, target: DeliveryTarget): string {
|
|
|
88
138
|
|
|
89
139
|
/**
|
|
90
140
|
* Resolve the results file path for a target.
|
|
91
|
-
*
|
|
141
|
+
*
|
|
142
|
+
* Lookup order (first existing wins):
|
|
143
|
+
* 1. Per-target env-specific: `<gen>/<name>-test-result.<SUNGEN_ENV>.json`
|
|
144
|
+
* 2. Per-target base: `<gen>/<name>-test-result.json`
|
|
145
|
+
* 3. Global env-specific: `test-results/results.<SUNGEN_ENV>.json`
|
|
146
|
+
* 4. Global base: `test-results/results.json`
|
|
147
|
+
*
|
|
148
|
+
* The env-suffixed names mirror what `playwright.config.ts` writes when
|
|
149
|
+
* `SUNGEN_ENV` is set, so each locale run keeps its own results file.
|
|
92
150
|
*/
|
|
93
151
|
function resolveResultsPath(cwd: string, target: DeliveryTarget): string | null {
|
|
94
152
|
const genDir = generatedDir(cwd, target);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
153
|
+
const env = process.env.SUNGEN_ENV;
|
|
154
|
+
const candidates: string[] = [];
|
|
155
|
+
if (env) candidates.push(path.join(genDir, `${target.featureBaseName}-test-result.${env}.json`));
|
|
156
|
+
candidates.push(path.join(genDir, `${target.featureBaseName}-test-result.json`));
|
|
157
|
+
if (env) candidates.push(path.join(cwd, 'test-results', `results.${env}.json`));
|
|
158
|
+
candidates.push(path.join(cwd, 'test-results', 'results.json'));
|
|
159
|
+
for (const c of candidates) {
|
|
160
|
+
if (fs.existsSync(c)) return c;
|
|
161
|
+
}
|
|
99
162
|
return null;
|
|
100
163
|
}
|
|
101
164
|
|
|
165
|
+
/**
|
|
166
|
+
* A single locale variant of a delivery target.
|
|
167
|
+
*
|
|
168
|
+
* `locale === ''` denotes the base locale (file `<name>-test-result.json`,
|
|
169
|
+
* test-data without overlay). Non-empty values come from
|
|
170
|
+
* `<name>-test-result.<locale>.json` files found in the generated directory.
|
|
171
|
+
*
|
|
172
|
+
* `displayCode` is what we show in sheet names — `BASE_LOCALE.toUpperCase()`
|
|
173
|
+
* for base, the locale code uppercased otherwise (e.g. `EN`, `JA`, `EN-US`).
|
|
174
|
+
*/
|
|
175
|
+
interface LocaleVariant {
|
|
176
|
+
locale: string; // '' for base, 'en' / 'ja' / 'staging-ja' for variants
|
|
177
|
+
displayCode: string; // 'VI' / 'EN' / 'JA' — used in sheet name
|
|
178
|
+
resultsPath: string | null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Base locale assumed when no `SUNGEN_ENV` and no explicit override.
|
|
183
|
+
* Configurable later via `qa/.sungen-config.yaml` or similar — currently
|
|
184
|
+
* hardcoded since every existing sungen project uses Vietnamese as the
|
|
185
|
+
* source-of-truth `test-data/<feature>.yaml`.
|
|
186
|
+
*/
|
|
187
|
+
const DEFAULT_BASE_LOCALE = 'vi';
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Discover every locale variant available for a feature by scanning the
|
|
191
|
+
* generated directory for `<featureBaseName>-test-result*.json` files.
|
|
192
|
+
*
|
|
193
|
+
* Result always includes the base (locale = '') as the first entry — even if
|
|
194
|
+
* `<name>-test-result.json` doesn't exist, callers still need somewhere to
|
|
195
|
+
* fall back to (Pending status). Variants follow alphabetically by locale
|
|
196
|
+
* code, so the sheet order is deterministic across runs.
|
|
197
|
+
*/
|
|
198
|
+
function discoverLocaleVariants(cwd: string, target: DeliveryTarget): LocaleVariant[] {
|
|
199
|
+
const genDir = generatedDir(cwd, target);
|
|
200
|
+
const prefix = `${target.featureBaseName}-test-result`;
|
|
201
|
+
const variants: LocaleVariant[] = [];
|
|
202
|
+
|
|
203
|
+
const basePath = path.join(genDir, `${prefix}.json`);
|
|
204
|
+
variants.push({
|
|
205
|
+
locale: '',
|
|
206
|
+
displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
|
|
207
|
+
resultsPath: fs.existsSync(basePath) ? basePath : null,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (fs.existsSync(genDir)) {
|
|
211
|
+
const localeFiles = fs.readdirSync(genDir)
|
|
212
|
+
.filter((f) => f.startsWith(`${prefix}.`) && f.endsWith('.json') && f !== `${prefix}.json`)
|
|
213
|
+
.sort();
|
|
214
|
+
for (const f of localeFiles) {
|
|
215
|
+
// Strip prefix + '.' on the left, '.json' on the right → locale code.
|
|
216
|
+
const locale = f.slice(prefix.length + 1, -'.json'.length);
|
|
217
|
+
if (!locale) continue;
|
|
218
|
+
variants.push({
|
|
219
|
+
locale,
|
|
220
|
+
displayCode: locale.toUpperCase(),
|
|
221
|
+
resultsPath: path.join(genDir, f),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return variants;
|
|
227
|
+
}
|
|
228
|
+
|
|
102
229
|
function resolveTestDataPathForTarget(cwd: string, target: DeliveryTarget): string {
|
|
103
|
-
return path.join(qaDir(cwd, target), 'test-data', `${target.
|
|
230
|
+
return path.join(qaDir(cwd, target), 'test-data', `${target.featureBaseName}.yaml`);
|
|
104
231
|
}
|
|
105
232
|
|
|
106
233
|
/**
|
|
@@ -126,42 +253,45 @@ function readFlowPagePath(selectorsFile: string): string | undefined {
|
|
|
126
253
|
function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
|
|
127
254
|
const base = qaDir(cwd, target);
|
|
128
255
|
const genBase = generatedDir(cwd, target);
|
|
129
|
-
const featureFile = path.join(base, 'features', `${target.
|
|
256
|
+
const featureFile = path.join(base, 'features', `${target.featureBaseName}.feature`);
|
|
130
257
|
const testDataFile = resolveTestDataPathForTarget(cwd, target);
|
|
131
|
-
const selectorsFile = path.join(base, 'selectors', `${target.
|
|
132
|
-
const specFile = path.join(genBase, `${target.
|
|
258
|
+
const selectorsFile = path.join(base, 'selectors', `${target.featureBaseName}.yaml`);
|
|
259
|
+
const specFile = path.join(genBase, `${target.featureBaseName}.spec.ts`);
|
|
133
260
|
const resultsFile = resolveResultsPath(cwd, target);
|
|
134
261
|
|
|
135
262
|
const featureOk = checkFeatureReal(featureFile);
|
|
136
263
|
const testDataOk = checkTestDataHasVars(testDataFile);
|
|
137
|
-
const selectorsOk = checkSelectorsHasEntries(selectorsFile, target.
|
|
264
|
+
const selectorsOk = checkSelectorsHasEntries(selectorsFile, target.featureBaseName);
|
|
138
265
|
const specOk = fs.existsSync(specFile);
|
|
139
266
|
const resultsOk = resultsFile !== null;
|
|
140
267
|
|
|
141
|
-
const label = target.isFlow ? `flow/${target.
|
|
268
|
+
const label = target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName;
|
|
142
269
|
const missing: string[] = [];
|
|
143
270
|
const suggestions: string[] = [];
|
|
144
271
|
|
|
145
272
|
if (!featureOk) {
|
|
146
273
|
missing.push(`feature file missing/empty: ${path.relative(cwd, featureFile)}`);
|
|
147
|
-
suggestions.push(`/sungen:create-test ${target.
|
|
274
|
+
suggestions.push(`/sungen:create-test ${target.screen}`);
|
|
148
275
|
}
|
|
149
276
|
if (!testDataOk) {
|
|
150
277
|
missing.push(`test-data.yaml has no variables: ${path.relative(cwd, testDataFile)}`);
|
|
151
|
-
suggestions.push(`/sungen:create-test ${target.
|
|
278
|
+
suggestions.push(`/sungen:create-test ${target.screen}`);
|
|
152
279
|
}
|
|
153
280
|
if (!selectorsOk) {
|
|
154
281
|
missing.push(`selectors.yaml missing entries: ${path.relative(cwd, selectorsFile)}`);
|
|
155
|
-
suggestions.push(`/sungen:run-test ${target.
|
|
282
|
+
suggestions.push(`/sungen:run-test ${target.screen}`);
|
|
156
283
|
}
|
|
157
284
|
if (!specOk) {
|
|
158
285
|
missing.push(`compiled .spec.ts missing: ${path.relative(cwd, specFile)}`);
|
|
159
|
-
suggestions.push(target.isFlow ? `sungen generate --flow ${target.
|
|
286
|
+
suggestions.push(target.isFlow ? `sungen generate --flow ${target.screen}` : `sungen generate --screen ${target.screen}`);
|
|
160
287
|
}
|
|
161
288
|
if (!resultsOk) {
|
|
162
|
-
|
|
289
|
+
const env = process.env.SUNGEN_ENV;
|
|
290
|
+
const envSuffix = env ? `.${env}` : '';
|
|
291
|
+
missing.push(`test-result${envSuffix}.json missing (optional)`);
|
|
163
292
|
const genRel = path.relative(cwd, genBase);
|
|
164
|
-
|
|
293
|
+
const envPrefix = env ? `SUNGEN_ENV=${env} ` : '';
|
|
294
|
+
suggestions.push(`${envPrefix}PLAYWRIGHT_JSON_OUTPUT_NAME=${genRel}/${target.featureBaseName}-test-result.json npx playwright test ${genRel}/${target.featureBaseName}.spec.ts`);
|
|
165
295
|
}
|
|
166
296
|
|
|
167
297
|
return {
|
|
@@ -247,45 +377,102 @@ async function exportTarget(
|
|
|
247
377
|
): Promise<ScreenSummary | null> {
|
|
248
378
|
const base = qaDir(cwd, target);
|
|
249
379
|
const genBase = generatedDir(cwd, target);
|
|
250
|
-
const featureFile = path.join(base, 'features', `${target.
|
|
380
|
+
const featureFile = path.join(base, 'features', `${target.featureBaseName}.feature`);
|
|
251
381
|
const testDataFile = resolveTestDataPathForTarget(cwd, target);
|
|
252
|
-
const specFile = path.join(genBase, `${target.
|
|
253
|
-
const resultsFile = resolveResultsPath(cwd, target);
|
|
382
|
+
const specFile = path.join(genBase, `${target.featureBaseName}.spec.ts`);
|
|
254
383
|
const specMdFile = path.join(base, 'requirements', 'spec.md');
|
|
255
|
-
const label = target.isFlow ? `flow/${target.
|
|
384
|
+
const label = target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName;
|
|
256
385
|
|
|
257
386
|
try {
|
|
258
387
|
const feature = parseFeatureMetadata(featureFile);
|
|
259
388
|
const spec = parseSpecFile(specFile);
|
|
260
|
-
const testData = loadTestData(testDataFile);
|
|
261
|
-
const results = resultsFile ? loadPlaywrightReport(resultsFile) : null;
|
|
262
389
|
|
|
263
390
|
const merged = mergeFeatureAndSpec(feature, spec);
|
|
264
391
|
// Screens get their URL path from the .feature file directly. Flows record
|
|
265
|
-
// it in selectors/<
|
|
266
|
-
// that so the Environment column shows a full URL for flows too.
|
|
392
|
+
// it in selectors/<feature>.yaml under the page-selector block — fall back
|
|
393
|
+
// to that so the Environment column shows a full URL for flows too.
|
|
267
394
|
let featurePath = feature.featurePath;
|
|
395
|
+
const selectorsFile = path.join(base, 'selectors', `${target.featureBaseName}.yaml`);
|
|
268
396
|
if (!featurePath && target.isFlow) {
|
|
269
|
-
const selectorsFile = path.join(base, 'selectors', `${target.name}.yaml`);
|
|
270
397
|
featurePath = readFlowPagePath(selectorsFile);
|
|
271
398
|
}
|
|
272
|
-
const
|
|
273
|
-
screen: label,
|
|
274
|
-
featureName: feature.featureName,
|
|
275
|
-
featurePath,
|
|
276
|
-
merged,
|
|
277
|
-
testData,
|
|
278
|
-
results,
|
|
279
|
-
env,
|
|
280
|
-
});
|
|
399
|
+
const selectorKeyMap = loadSelectorKeyMap(selectorsFile);
|
|
281
400
|
|
|
282
401
|
const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : '';
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
402
|
+
const explicitEnv = process.env.SUNGEN_ENV;
|
|
403
|
+
|
|
404
|
+
// Decide between single-locale and aggregated multi-locale flows.
|
|
405
|
+
// • SUNGEN_ENV set → single locale (existing behaviour, no change)
|
|
406
|
+
// • SUNGEN_ENV unset → discover every *-test-result*.json variant.
|
|
407
|
+
// 2+ variants → XLSX with one sheet per locale (`<feature>-<LOCALE>`),
|
|
408
|
+
// one CSV per locale (CSV has no sheet concept).
|
|
409
|
+
// <2 variants → single-sheet XLSX with the canonical 'Testcases' sheet.
|
|
410
|
+
if (explicitEnv) {
|
|
411
|
+
const rows = buildTestCaseRows({
|
|
412
|
+
screen: label,
|
|
413
|
+
featureName: feature.featureName,
|
|
414
|
+
featurePath,
|
|
415
|
+
merged,
|
|
416
|
+
testData: loadTestData(testDataFile),
|
|
417
|
+
results: resolveResultsPath(cwd, target) ? loadPlaywrightReport(resolveResultsPath(cwd, target)!) : null,
|
|
418
|
+
env,
|
|
419
|
+
selectorKeyMap,
|
|
420
|
+
});
|
|
421
|
+
const tempSummary = buildSummary(label, rows, '');
|
|
422
|
+
const csv = renderCsv(tempSummary, rows, specLink);
|
|
423
|
+
const csvPath = writeCsv(cwd, target.featureBaseName, csv);
|
|
424
|
+
const wb = renderXlsx(tempSummary, rows, specLink);
|
|
425
|
+
await writeXlsx(cwd, target.featureBaseName, wb);
|
|
426
|
+
return buildSummary(label, rows, path.relative(cwd, csvPath));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const variants = discoverLocaleVariants(cwd, target);
|
|
430
|
+
let primarySummary: ScreenSummary | null = null;
|
|
431
|
+
let primaryCsvPath = '';
|
|
432
|
+
const sheets: { sheetName: string; summary: ScreenSummary; rows: TestCaseRow[]; specLink: string }[] = [];
|
|
433
|
+
|
|
434
|
+
for (const variant of variants) {
|
|
435
|
+
// For the base variant the overlay merge is skipped (`locale: null`);
|
|
436
|
+
// for locale variants the matching `<feature>.<locale>.yaml` is merged.
|
|
437
|
+
const variantTestData = loadTestData(testDataFile, variant.locale || null);
|
|
438
|
+
const variantResults = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
|
|
439
|
+
const variantRows = buildTestCaseRows({
|
|
440
|
+
screen: label,
|
|
441
|
+
featureName: feature.featureName,
|
|
442
|
+
featurePath,
|
|
443
|
+
merged,
|
|
444
|
+
testData: variantTestData,
|
|
445
|
+
results: variantResults,
|
|
446
|
+
env,
|
|
447
|
+
selectorKeyMap,
|
|
448
|
+
});
|
|
449
|
+
const variantSummary = buildSummary(label, variantRows, '');
|
|
450
|
+
|
|
451
|
+
// CSV: always one file per locale (CSV has no sheet concept).
|
|
452
|
+
const csvLocale = variant.locale || null; // '' or 'en' → '' / 'en'
|
|
453
|
+
const csv = renderCsv(variantSummary, variantRows, specLink);
|
|
454
|
+
const csvPath = writeCsv(cwd, target.featureBaseName, csv, csvLocale);
|
|
455
|
+
|
|
456
|
+
sheets.push({
|
|
457
|
+
sheetName: `${target.featureBaseName}-${variant.displayCode}`,
|
|
458
|
+
summary: variantSummary,
|
|
459
|
+
rows: variantRows,
|
|
460
|
+
specLink,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Use the base variant's summary as the "primary" return value so the
|
|
464
|
+
// top-level reporter rolls up base-locale numbers.
|
|
465
|
+
if (variant.locale === '') {
|
|
466
|
+
primarySummary = buildSummary(label, variantRows, path.relative(cwd, csvPath));
|
|
467
|
+
primaryCsvPath = csvPath;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// XLSX: single-sheet when only base, multi-sheet when 2+ locales found.
|
|
472
|
+
const wb = sheets.length >= 2 ? renderXlsxMultiSheet(sheets) : renderXlsx(sheets[0].summary, sheets[0].rows, specLink);
|
|
473
|
+
await writeXlsx(cwd, target.featureBaseName, wb);
|
|
474
|
+
|
|
475
|
+
return primarySummary ?? buildSummary(label, sheets[0].rows, primaryCsvPath);
|
|
289
476
|
} catch (err) {
|
|
290
477
|
console.error(`${COLOR.red}Error exporting ${label}:${COLOR.reset} ${err instanceof Error ? err.message : err}`);
|
|
291
478
|
return null;
|
|
@@ -301,10 +488,11 @@ function printPreflightTable(checks: PreflightCheck[]): void {
|
|
|
301
488
|
log(`${COLOR.gray}Columns: feature | test-data | selectors | .spec.ts | test-result.json${COLOR.reset}\n`);
|
|
302
489
|
|
|
303
490
|
const ok = (b: boolean) => (b ? `${COLOR.green}✓${COLOR.reset}` : `${COLOR.red}✗${COLOR.reset}`);
|
|
491
|
+
const colWidth = Math.max(20, ...checks.map((c) => c.screen.length)) + 2;
|
|
304
492
|
|
|
305
493
|
for (const c of checks) {
|
|
306
494
|
log(
|
|
307
|
-
` ${c.screen.padEnd(
|
|
495
|
+
` ${c.screen.padEnd(colWidth)}${ok(c.featureOk)} ${ok(c.testDataOk)} ${ok(c.selectorsOk)} ${ok(c.specOk)} ${ok(c.resultsOk)}`
|
|
308
496
|
);
|
|
309
497
|
}
|
|
310
498
|
log('');
|
|
@@ -335,11 +523,12 @@ function printPreflightTable(checks: PreflightCheck[]): void {
|
|
|
335
523
|
function printSummaryTable(summaries: ScreenSummary[]): void {
|
|
336
524
|
log(`\n${COLOR.bold}Delivery export complete${COLOR.reset}`);
|
|
337
525
|
log('');
|
|
338
|
-
|
|
339
|
-
log(
|
|
526
|
+
const colWidth = Math.max(20, ...summaries.map((s) => s.screen.length)) + 1;
|
|
527
|
+
log(` ${'Feature'.padEnd(colWidth)}TCs Passed Failed Pending N/A File`);
|
|
528
|
+
log(' ' + '-'.repeat(colWidth + 60));
|
|
340
529
|
for (const s of summaries) {
|
|
341
530
|
log(
|
|
342
|
-
` ${s.screen.padEnd(
|
|
531
|
+
` ${s.screen.padEnd(colWidth)}${String(s.total).padStart(3)} ${String(s.passed).padStart(6)} ${String(s.failed).padStart(6)} ${String(s.pending).padStart(7)} ${String(s.na).padStart(3)} ${s.outputFile}`
|
|
343
532
|
);
|
|
344
533
|
}
|
|
345
534
|
log('');
|
|
@@ -365,20 +554,22 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
365
554
|
try {
|
|
366
555
|
const cwd = process.cwd();
|
|
367
556
|
|
|
368
|
-
// 1. Scope detection
|
|
557
|
+
// 1. Scope detection — each positional name expands into one target
|
|
558
|
+
// per `.feature` file inside that screen/flow. Passing a specific
|
|
559
|
+
// feature basename (e.g. `home-modal`) narrows to that one file.
|
|
369
560
|
let targets: DeliveryTarget[];
|
|
370
561
|
if (names && names.length > 0) {
|
|
371
|
-
targets = names.
|
|
562
|
+
targets = names.flatMap((n) => resolveTargetsFromArg(cwd, n));
|
|
372
563
|
} else {
|
|
373
564
|
targets = listAllTargets(cwd);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
565
|
+
}
|
|
566
|
+
if (targets.length === 0) {
|
|
567
|
+
console.error(`${COLOR.red}No .feature files found in qa/screens/ or qa/flows/${COLOR.reset}`);
|
|
568
|
+
process.exit(1);
|
|
378
569
|
}
|
|
379
570
|
|
|
380
|
-
const labels = targets.map((t) => t.isFlow ? `flow/${t.
|
|
381
|
-
log(`${COLOR.bold}sungen delivery${COLOR.reset} — exporting ${targets.length}
|
|
571
|
+
const labels = targets.map((t) => t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName);
|
|
572
|
+
log(`${COLOR.bold}sungen delivery${COLOR.reset} — exporting ${targets.length} feature(s): ${labels.join(', ')}\n`);
|
|
382
573
|
|
|
383
574
|
// 2. Pre-flight
|
|
384
575
|
let toExport: DeliveryTarget[];
|
|
@@ -395,7 +586,7 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
395
586
|
checks.filter((c) => !hasBlockingMissing(c)).map((c) => c.screen)
|
|
396
587
|
);
|
|
397
588
|
toExport = targets.filter((t) => {
|
|
398
|
-
const label = t.isFlow ? `flow/${t.
|
|
589
|
+
const label = t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName;
|
|
399
590
|
return passedScreens.has(label);
|
|
400
591
|
});
|
|
401
592
|
log(`${COLOR.yellow}Continuing with ${toExport.length} ready target(s).${COLOR.reset}\n`);
|