@sun-asterisk/sungen 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/dist/cli/commands/delivery.d.ts +7 -0
  2. package/dist/cli/commands/delivery.d.ts.map +1 -0
  3. package/dist/cli/commands/delivery.js +348 -0
  4. package/dist/cli/commands/delivery.js.map +1 -0
  5. package/dist/cli/commands/generate.d.ts.map +1 -1
  6. package/dist/cli/commands/generate.js +2 -0
  7. package/dist/cli/commands/generate.js.map +1 -1
  8. package/dist/cli/commands/update.d.ts.map +1 -1
  9. package/dist/cli/commands/update.js +64 -1
  10. package/dist/cli/commands/update.js.map +1 -1
  11. package/dist/cli/index.js +4 -2
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/exporters/csv-exporter.d.ts +32 -0
  14. package/dist/exporters/csv-exporter.d.ts.map +1 -0
  15. package/dist/exporters/csv-exporter.js +311 -0
  16. package/dist/exporters/csv-exporter.js.map +1 -0
  17. package/dist/exporters/feature-parser.d.ts +48 -0
  18. package/dist/exporters/feature-parser.d.ts.map +1 -0
  19. package/dist/exporters/feature-parser.js +178 -0
  20. package/dist/exporters/feature-parser.js.map +1 -0
  21. package/dist/exporters/package-info.d.ts +9 -0
  22. package/dist/exporters/package-info.d.ts.map +1 -0
  23. package/dist/exporters/package-info.js +73 -0
  24. package/dist/exporters/package-info.js.map +1 -0
  25. package/dist/exporters/playwright-report-parser.d.ts +21 -0
  26. package/dist/exporters/playwright-report-parser.d.ts.map +1 -0
  27. package/dist/exporters/playwright-report-parser.js +184 -0
  28. package/dist/exporters/playwright-report-parser.js.map +1 -0
  29. package/dist/exporters/scenario-merger.d.ts +21 -0
  30. package/dist/exporters/scenario-merger.d.ts.map +1 -0
  31. package/dist/exporters/scenario-merger.js +51 -0
  32. package/dist/exporters/scenario-merger.js.map +1 -0
  33. package/dist/exporters/spec-parser.d.ts +20 -0
  34. package/dist/exporters/spec-parser.d.ts.map +1 -0
  35. package/dist/exporters/spec-parser.js +259 -0
  36. package/dist/exporters/spec-parser.js.map +1 -0
  37. package/dist/exporters/step-formatter.d.ts +32 -0
  38. package/dist/exporters/step-formatter.d.ts.map +1 -0
  39. package/dist/exporters/step-formatter.js +76 -0
  40. package/dist/exporters/step-formatter.js.map +1 -0
  41. package/dist/exporters/test-data-resolver.d.ts +20 -0
  42. package/dist/exporters/test-data-resolver.d.ts.map +1 -0
  43. package/dist/exporters/test-data-resolver.js +96 -0
  44. package/dist/exporters/test-data-resolver.js.map +1 -0
  45. package/dist/exporters/types.d.ts +104 -0
  46. package/dist/exporters/types.d.ts.map +1 -0
  47. package/dist/exporters/types.js +6 -0
  48. package/dist/exporters/types.js.map +1 -0
  49. package/dist/exporters/xlsx-exporter.d.ts +19 -0
  50. package/dist/exporters/xlsx-exporter.d.ts.map +1 -0
  51. package/dist/exporters/xlsx-exporter.js +309 -0
  52. package/dist/exporters/xlsx-exporter.js.map +1 -0
  53. package/dist/generators/gherkin-parser/index.d.ts +1 -0
  54. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  55. package/dist/generators/gherkin-parser/index.js +3 -0
  56. package/dist/generators/gherkin-parser/index.js.map +1 -1
  57. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +29 -1
  58. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  59. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +21 -1
  60. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  61. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js +11 -2
  62. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  63. package/dist/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  64. package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  65. package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  66. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  67. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  68. package/dist/generators/test-generator/code-generator.d.ts +2 -0
  69. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  70. package/dist/generators/test-generator/code-generator.js +109 -12
  71. package/dist/generators/test-generator/code-generator.js.map +1 -1
  72. package/dist/generators/test-generator/step-mapper.d.ts +1 -0
  73. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  74. package/dist/generators/test-generator/step-mapper.js +1 -1
  75. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  76. package/dist/generators/test-generator/template-engine.d.ts +29 -1
  77. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  78. package/dist/generators/test-generator/template-engine.js +11 -2
  79. package/dist/generators/test-generator/template-engine.js.map +1 -1
  80. package/dist/generators/test-generator/utils/data-resolver.d.ts +11 -2
  81. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  82. package/dist/generators/test-generator/utils/data-resolver.js +36 -25
  83. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  84. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +7 -0
  85. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -0
  86. package/dist/generators/test-generator/utils/runtime-data-transformer.js +42 -0
  87. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -0
  88. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  89. package/dist/generators/test-generator/utils/selector-resolver.js +26 -0
  90. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  91. package/dist/generators/types.d.ts +1 -0
  92. package/dist/generators/types.d.ts.map +1 -1
  93. package/dist/generators/types.js.map +1 -1
  94. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  95. package/dist/orchestrator/ai-rules-updater.js +12 -0
  96. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  97. package/dist/orchestrator/project-initializer.d.ts +21 -1
  98. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  99. package/dist/orchestrator/project-initializer.js +158 -74
  100. package/dist/orchestrator/project-initializer.js.map +1 -1
  101. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  102. package/dist/orchestrator/screen-manager.js +2 -0
  103. package/dist/orchestrator/screen-manager.js.map +1 -1
  104. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +15 -17
  105. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +7 -5
  106. package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +71 -0
  107. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +28 -1
  108. package/dist/orchestrator/templates/ai-instructions/claude-config.md +23 -4
  109. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +142 -0
  110. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +100 -0
  111. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +73 -0
  112. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +103 -0
  113. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +68 -13
  114. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
  115. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +54 -3
  116. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -15
  117. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -4
  118. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +71 -0
  119. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -14
  120. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +23 -4
  121. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +142 -0
  122. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +100 -0
  123. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +73 -0
  124. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +103 -0
  125. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +88 -13
  126. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +22 -0
  127. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +54 -3
  128. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  129. package/dist/orchestrator/templates/playwright.config.js +6 -1
  130. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  131. package/dist/orchestrator/templates/playwright.config.ts +6 -1
  132. package/dist/orchestrator/templates/specs-base.d.ts +12 -1
  133. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  134. package/dist/orchestrator/templates/specs-base.js +47 -5
  135. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  136. package/dist/orchestrator/templates/specs-base.ts +65 -7
  137. package/dist/orchestrator/templates/specs-test-data.d.ts +14 -0
  138. package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -0
  139. package/dist/orchestrator/templates/specs-test-data.js +100 -0
  140. package/dist/orchestrator/templates/specs-test-data.js.map +1 -0
  141. package/dist/orchestrator/templates/specs-test-data.ts +66 -0
  142. package/package.json +2 -1
  143. package/src/cli/commands/delivery.ts +348 -0
  144. package/src/cli/commands/generate.ts +2 -0
  145. package/src/cli/commands/update.ts +84 -2
  146. package/src/cli/index.ts +4 -2
  147. package/src/exporters/csv-exporter.ts +304 -0
  148. package/src/exporters/feature-parser.ts +168 -0
  149. package/src/exporters/package-info.ts +35 -0
  150. package/src/exporters/playwright-report-parser.ts +168 -0
  151. package/src/exporters/scenario-merger.ts +63 -0
  152. package/src/exporters/spec-parser.ts +247 -0
  153. package/src/exporters/step-formatter.ts +80 -0
  154. package/src/exporters/test-data-resolver.ts +59 -0
  155. package/src/exporters/types.ts +112 -0
  156. package/src/exporters/xlsx-exporter.ts +301 -0
  157. package/src/generators/gherkin-parser/index.ts +4 -0
  158. package/src/generators/test-generator/adapters/adapter-interface.ts +12 -1
  159. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +14 -2
  160. package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  161. package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  162. package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  163. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  164. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  165. package/src/generators/test-generator/code-generator.ts +122 -13
  166. package/src/generators/test-generator/step-mapper.ts +2 -2
  167. package/src/generators/test-generator/template-engine.ts +28 -2
  168. package/src/generators/test-generator/utils/data-resolver.ts +45 -27
  169. package/src/generators/test-generator/utils/runtime-data-transformer.ts +51 -0
  170. package/src/generators/test-generator/utils/selector-resolver.ts +26 -0
  171. package/src/generators/types.ts +1 -0
  172. package/src/orchestrator/ai-rules-updater.ts +12 -0
  173. package/src/orchestrator/project-initializer.ts +187 -80
  174. package/src/orchestrator/screen-manager.ts +2 -0
  175. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +15 -17
  176. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +7 -5
  177. package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +71 -0
  178. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +28 -1
  179. package/src/orchestrator/templates/ai-instructions/claude-config.md +23 -4
  180. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +142 -0
  181. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +100 -0
  182. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +73 -0
  183. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +103 -0
  184. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +68 -13
  185. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +22 -0
  186. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +54 -3
  187. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -15
  188. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +6 -4
  189. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +71 -0
  190. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -14
  191. package/src/orchestrator/templates/ai-instructions/copilot-config.md +23 -4
  192. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +142 -0
  193. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +100 -0
  194. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +73 -0
  195. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +103 -0
  196. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +88 -13
  197. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +22 -0
  198. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +54 -3
  199. package/src/orchestrator/templates/playwright.config.ts +6 -1
  200. package/src/orchestrator/templates/specs-base.ts +65 -7
  201. package/src/orchestrator/templates/specs-test-data.ts +66 -0
@@ -58,6 +58,7 @@ export function registerGenerateCommand(program: Command): void {
58
58
  .option('-s, --screen <name>', 'Generate tests for a specific screen')
59
59
  .option('--all', 'Generate tests for all screens')
60
60
  .option('--framework <name>', 'Test framework (default: playwright)', 'playwright')
61
+ .option('--inline-data', 'Hardcode test data at compile time instead of runtime loading')
61
62
  .action(async (options) => {
62
63
  try {
63
64
  const screenName = options.screen;
@@ -89,6 +90,7 @@ export function registerGenerateCommand(program: Command): void {
89
90
  framework: options.framework || 'playwright',
90
91
  screenName,
91
92
  verbose: program.opts().verbose,
93
+ runtimeData: !options.inlineData,
92
94
  });
93
95
 
94
96
  const results = await generator.generateAllTests(
@@ -1,12 +1,52 @@
1
1
  import { Command } from 'commander';
2
+ import { spawnSync } from 'child_process';
3
+
4
+ /**
5
+ * `sungen update` does two jobs in sequence:
6
+ *
7
+ * 1. Reinstall `@sun-asterisk/sungen@latest` globally via npm so the bundled
8
+ * AI templates are refreshed.
9
+ * 2. Re-execute `sungen update` with the env var `SUNGEN_UPDATE_SKIP_NPM=1`
10
+ * so the AI rules / commands / skills inside the project get overwritten
11
+ * from the *new* templates.
12
+ *
13
+ * We use an environment variable (not a CLI flag) for the internal hand-off
14
+ * because Commander throws "unknown option" if the currently-installed
15
+ * published version doesn't recognise the flag yet. Env vars are silently
16
+ * ignored by older versions — the worst case is one extra (idempotent) npm
17
+ * install, never a crash.
18
+ *
19
+ * The public `--skip-npm-install` flag is still available for users who
20
+ * already installed the binary by other means (CI pipelines, corporate
21
+ * mirror, manual download) and only want to refresh the project AI assets.
22
+ */
23
+
24
+ const SKIP_NPM_ENV = 'SUNGEN_UPDATE_SKIP_NPM';
2
25
 
3
26
  export function registerUpdateCommand(program: Command): void {
4
27
  program
5
28
  .command('update')
6
- .description('Update AI rules, commands, and skills to the latest version')
29
+ .description(
30
+ 'Reinstall @sun-asterisk/sungen@latest + refresh AI rules, commands, and skills',
31
+ )
7
32
  .option('--dry-run', 'Show what would be updated without making changes')
8
- .action(async (options: { dryRun?: boolean }) => {
33
+ .option(
34
+ '--skip-npm-install',
35
+ 'Skip the `npm install -g @sun-asterisk/sungen@latest` step (refresh project AI assets only)',
36
+ false,
37
+ )
38
+ .action(async (options: { dryRun?: boolean; skipNpmInstall?: boolean }) => {
9
39
  try {
40
+ const skipNpm =
41
+ Boolean(options.skipNpmInstall) || process.env[SKIP_NPM_ENV] === '1';
42
+
43
+ if (!skipNpm) {
44
+ reinstallLatestSungen();
45
+ printCurrentVersion();
46
+ reExecUpdateForAIAssets(options.dryRun ?? false);
47
+ return;
48
+ }
49
+
10
50
  const { AIRulesUpdater } = require('../../orchestrator/ai-rules-updater');
11
51
  const updater = new AIRulesUpdater(process.cwd());
12
52
  await updater.update(options.dryRun ?? false);
@@ -16,3 +56,45 @@ export function registerUpdateCommand(program: Command): void {
16
56
  }
17
57
  });
18
58
  }
59
+
60
+ function reinstallLatestSungen(): void {
61
+ console.log('📦 Installing @sun-asterisk/sungen@latest...');
62
+ const result = spawnSync('npm', ['install', '-g', '@sun-asterisk/sungen@latest'], {
63
+ stdio: 'inherit',
64
+ shell: true,
65
+ });
66
+ if (result.status !== 0) {
67
+ throw new Error(
68
+ 'npm install -g @sun-asterisk/sungen@latest failed. Run it manually or check your npm setup.',
69
+ );
70
+ }
71
+ }
72
+
73
+ function printCurrentVersion(): void {
74
+ console.log('\n🔎 Installed version:');
75
+ spawnSync('sungen', ['--version'], { stdio: 'inherit', shell: true });
76
+ console.log('');
77
+ }
78
+
79
+ function reExecUpdateForAIAssets(dryRun: boolean): void {
80
+ const args = ['update'];
81
+ if (dryRun) args.push('--dry-run');
82
+
83
+ const result = spawnSync('sungen', args, {
84
+ stdio: 'inherit',
85
+ shell: true,
86
+ env: { ...process.env, [SKIP_NPM_ENV]: '1' },
87
+ });
88
+
89
+ if (result.status !== 0) {
90
+ console.error(
91
+ '\n⚠️ AI-rules refresh step returned a non-zero exit code.\n' +
92
+ ` If the newly installed version is older and didn't recognise this flow,\n` +
93
+ ' run the refresh manually:\n\n' +
94
+ ' sungen update --skip-npm-install' +
95
+ (dryRun ? ' --dry-run' : '') +
96
+ '\n',
97
+ );
98
+ }
99
+ process.exit(result.status ?? 0);
100
+ }
package/src/cli/index.ts CHANGED
@@ -10,6 +10,7 @@ import { registerAddCommand } from './commands/add';
10
10
  import { registerGenerateCommand } from './commands/generate';
11
11
  import { registerMakeauthCommand } from './commands/makeauth';
12
12
  import { registerUpdateCommand } from './commands/update';
13
+ import { registerDeliveryCommand } from './commands/delivery';
13
14
 
14
15
  async function main() {
15
16
  const program = new Command();
@@ -17,18 +18,19 @@ async function main() {
17
18
  program
18
19
  .name('sungen')
19
20
  .description('Deterministic E2E Test Compiler — Gherkin + Selectors → Playwright')
20
- .version('2.4.5');
21
+ .version('2.5.0');
21
22
 
22
23
  // Global options
23
24
  program
24
25
  .option('-v, --verbose', 'Enable verbose logging');
25
26
 
26
- // Register commands (5 only)
27
+ // Register commands (6)
27
28
  registerInitCommand(program);
28
29
  registerAddCommand(program);
29
30
  registerGenerateCommand(program);
30
31
  registerMakeauthCommand(program);
31
32
  registerUpdateCommand(program);
33
+ registerDeliveryCommand(program);
32
34
 
33
35
  await program.parseAsync(process.argv);
34
36
  }
@@ -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
+ }