@sun-asterisk/sungen 2.6.8 → 2.6.11

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 (89) hide show
  1. package/dist/cli/commands/dashboard.d.ts +2 -1
  2. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  3. package/dist/cli/commands/dashboard.js +9 -9
  4. package/dist/cli/commands/dashboard.js.map +1 -1
  5. package/dist/cli/commands/delivery.d.ts.map +1 -1
  6. package/dist/cli/commands/delivery.js +33 -0
  7. package/dist/cli/commands/delivery.js.map +1 -1
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/dashboard/history-store.d.ts +13 -9
  11. package/dist/dashboard/history-store.d.ts.map +1 -1
  12. package/dist/dashboard/history-store.js +19 -28
  13. package/dist/dashboard/history-store.js.map +1 -1
  14. package/dist/dashboard/html-renderer.d.ts +1 -1
  15. package/dist/dashboard/html-renderer.d.ts.map +1 -1
  16. package/dist/dashboard/html-renderer.js +2 -2
  17. package/dist/dashboard/html-renderer.js.map +1 -1
  18. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  19. package/dist/dashboard/snapshot-builder.js +38 -2
  20. package/dist/dashboard/snapshot-builder.js.map +1 -1
  21. package/dist/dashboard/templates/index.html +153 -221
  22. package/dist/exporters/csv-exporter.d.ts +4 -0
  23. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  24. package/dist/exporters/csv-exporter.js +35 -26
  25. package/dist/exporters/csv-exporter.js.map +1 -1
  26. package/dist/exporters/feature-parser.d.ts.map +1 -1
  27. package/dist/exporters/feature-parser.js +16 -4
  28. package/dist/exporters/feature-parser.js.map +1 -1
  29. package/dist/exporters/json-exporter.d.ts.map +1 -1
  30. package/dist/exporters/json-exporter.js +28 -20
  31. package/dist/exporters/json-exporter.js.map +1 -1
  32. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  33. package/dist/exporters/playwright-report-parser.js +22 -5
  34. package/dist/exporters/playwright-report-parser.js.map +1 -1
  35. package/dist/exporters/scenario-merger.d.ts +23 -1
  36. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  37. package/dist/exporters/scenario-merger.js +39 -0
  38. package/dist/exporters/scenario-merger.js.map +1 -1
  39. package/dist/exporters/step-formatter.d.ts +31 -3
  40. package/dist/exporters/step-formatter.d.ts.map +1 -1
  41. package/dist/exporters/step-formatter.js +52 -19
  42. package/dist/exporters/step-formatter.js.map +1 -1
  43. package/dist/exporters/sun-logo.d.ts +10 -0
  44. package/dist/exporters/sun-logo.d.ts.map +1 -0
  45. package/dist/exporters/sun-logo.js +13 -0
  46. package/dist/exporters/sun-logo.js.map +1 -0
  47. package/dist/exporters/test-data-resolver.d.ts +13 -5
  48. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  49. package/dist/exporters/test-data-resolver.js +36 -14
  50. package/dist/exporters/test-data-resolver.js.map +1 -1
  51. package/dist/exporters/types.d.ts +16 -0
  52. package/dist/exporters/types.d.ts.map +1 -1
  53. package/dist/exporters/xlsx-exporter.d.ts +6 -0
  54. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  55. package/dist/exporters/xlsx-exporter.js +204 -100
  56. package/dist/exporters/xlsx-exporter.js.map +1 -1
  57. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  58. package/dist/orchestrator/project-initializer.js +4 -3
  59. package/dist/orchestrator/project-initializer.js.map +1 -1
  60. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  61. package/dist/orchestrator/templates/playwright.config.js +2 -0
  62. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  63. package/dist/orchestrator/templates/playwright.config.ts +2 -0
  64. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  65. package/dist/orchestrator/templates/specs-base.js +1 -5
  66. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  67. package/dist/orchestrator/templates/specs-base.ts +1 -5
  68. package/package.json +1 -1
  69. package/src/cli/commands/dashboard.ts +9 -9
  70. package/src/cli/commands/delivery.ts +30 -0
  71. package/src/cli/index.ts +1 -1
  72. package/src/dashboard/history-store.ts +22 -28
  73. package/src/dashboard/html-renderer.ts +6 -2
  74. package/src/dashboard/snapshot-builder.ts +36 -2
  75. package/src/dashboard/templates/index.html +153 -221
  76. package/src/dashboard/types.ts +1 -1
  77. package/src/exporters/csv-exporter.ts +44 -27
  78. package/src/exporters/feature-parser.ts +27 -8
  79. package/src/exporters/json-exporter.ts +31 -21
  80. package/src/exporters/playwright-report-parser.ts +23 -5
  81. package/src/exporters/scenario-merger.ts +65 -1
  82. package/src/exporters/step-formatter.ts +48 -23
  83. package/src/exporters/sun-logo.ts +10 -0
  84. package/src/exporters/test-data-resolver.ts +37 -13
  85. package/src/exporters/types.ts +18 -1
  86. package/src/exporters/xlsx-exporter.ts +216 -102
  87. package/src/orchestrator/project-initializer.ts +4 -3
  88. package/src/orchestrator/templates/playwright.config.ts +2 -0
  89. package/src/orchestrator/templates/specs-base.ts +1 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "2.6.8",
3
+ "version": "2.6.11",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Builds qa/dashboard/index.html — a single-file, share-ready test report —
5
5
  * from existing Gherkin features, compiled .spec.ts files, and Playwright
6
- * results. Snapshots are persisted under qa/dashboard/history/ (max 20).
6
+ * results. Snapshots are persisted under qa/dashboard/history/ (never deleted;
7
+ * --max-history only controls how many are included in dashboard stats).
7
8
  */
8
9
 
9
10
  import { Command } from 'commander';
@@ -68,7 +69,7 @@ export function registerDashboardCommand(program: Command): void {
68
69
  .description('Build a single-file HTML dashboard summarising all test cases & results')
69
70
  .argument('[names...]', 'Specific screen or flow names. Omit to include all.')
70
71
  .option('--no-history', 'Do not persist this run under qa/dashboard/history/')
71
- .option('--max-history <n>', `Cap retained history files (default: ${DEFAULT_MAX_HISTORY})`)
72
+ .option('--max-history <n>', `Limit snapshots included in dashboard stats (default: ${DEFAULT_MAX_HISTORY}). Older files are kept on disk.`)
72
73
  .option('--open', 'Open the rendered HTML in the default browser when done')
73
74
  .action(async (names: string[], options: DashboardOptions) => {
74
75
  try {
@@ -103,18 +104,17 @@ export function registerDashboardCommand(program: Command): void {
103
104
  `${COLOR.gray}(${snapshot.screens.length} screens, ${snapshot.summary.total} TCs)${COLOR.reset}`
104
105
  );
105
106
 
106
- // 3. Persist history (unless disabled)
107
+ // 3. Persist history (unless disabled). Files are never deleted —
108
+ // `max` only controls how many feed into the dashboard payload.
109
+ const max = options.maxHistory ? Math.max(1, parseInt(options.maxHistory, 10) || DEFAULT_MAX_HISTORY) : DEFAULT_MAX_HISTORY;
107
110
  if (!options.noHistory) {
108
- const max = options.maxHistory ? Math.max(1, parseInt(options.maxHistory, 10) || DEFAULT_MAX_HISTORY) : DEFAULT_MAX_HISTORY;
109
111
  const result = writeSnapshotToHistory(cwd, snapshot, max);
110
112
  log(` ${COLOR.green}✓${COLOR.reset} history ${path.relative(cwd, result.written)}`);
111
- if (result.pruned.length > 0) {
112
- log(` ${COLOR.gray} pruned ${result.pruned.length} older snapshot(s)${COLOR.reset}`);
113
- }
113
+ log(` ${COLOR.gray} ${result.total} on disk · ${result.included.length} within limit (${max})${result.archived.length > 0 ? ` · ${result.archived.length} archived (kept)` : ''}${COLOR.reset}`);
114
114
  }
115
115
 
116
- // 4. Render HTML
117
- const payload = buildPayload(cwd, snapshot);
116
+ // 4. Render HTML — limit how many history files feed the payload.
117
+ const payload = buildPayload(cwd, snapshot, max);
118
118
  const rendered = renderDashboardHtml(cwd, payload);
119
119
  log(` ${COLOR.green}✓${COLOR.reset} html ${path.relative(cwd, rendered.outputPath)} ${COLOR.gray}(${(rendered.bytes / 1024 / 1024).toFixed(2)} MB)${COLOR.reset}`);
120
120
 
@@ -7,6 +7,7 @@ import { Command } from 'commander';
7
7
  import * as path from 'path';
8
8
  import * as fs from 'fs';
9
9
  import { execSync } from 'child_process';
10
+ import { parse as parseYaml } from 'yaml';
10
11
  import { parseFeatureMetadata } from '../../exporters/feature-parser';
11
12
  import { parseSpecFile } from '../../exporters/spec-parser';
12
13
  import { loadTestData } from '../../exporters/test-data-resolver';
@@ -102,6 +103,26 @@ function resolveTestDataPathForTarget(cwd: string, target: DeliveryTarget): stri
102
103
  return path.join(qaDir(cwd, target), 'test-data', `${target.name}.yaml`);
103
104
  }
104
105
 
106
+ /**
107
+ * Flows record their entry-point URL inside selectors/<name>.yaml under the
108
+ * page-selector block (entry with `type: 'page'`). Return its `value`, or
109
+ * undefined when the file is missing or has no page selector.
110
+ */
111
+ function readFlowPagePath(selectorsFile: string): string | undefined {
112
+ if (!fs.existsSync(selectorsFile)) return undefined;
113
+ try {
114
+ const parsed = parseYaml(fs.readFileSync(selectorsFile, 'utf-8'));
115
+ if (!parsed || typeof parsed !== 'object') return undefined;
116
+ for (const v of Object.values(parsed as Record<string, unknown>)) {
117
+ if (v && typeof v === 'object' && (v as { type?: string }).type === 'page') {
118
+ const value = (v as { value?: unknown }).value;
119
+ if (typeof value === 'string' && value.length > 0) return value;
120
+ }
121
+ }
122
+ } catch { /* malformed YAML — ignore */ }
123
+ return undefined;
124
+ }
125
+
105
126
  function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
106
127
  const base = qaDir(cwd, target);
107
128
  const genBase = generatedDir(cwd, target);
@@ -240,9 +261,18 @@ async function exportTarget(
240
261
  const results = resultsFile ? loadPlaywrightReport(resultsFile) : null;
241
262
 
242
263
  const merged = mergeFeatureAndSpec(feature, spec);
264
+ // Screens get their URL path from the .feature file directly. Flows record
265
+ // it in selectors/<name>.yaml under the page-selector block — fall back to
266
+ // that so the Environment column shows a full URL for flows too.
267
+ let featurePath = feature.featurePath;
268
+ if (!featurePath && target.isFlow) {
269
+ const selectorsFile = path.join(base, 'selectors', `${target.name}.yaml`);
270
+ featurePath = readFlowPagePath(selectorsFile);
271
+ }
243
272
  const rows = buildTestCaseRows({
244
273
  screen: label,
245
274
  featureName: feature.featureName,
275
+ featurePath,
246
276
  merged,
247
277
  testData,
248
278
  results,
package/src/cli/index.ts CHANGED
@@ -21,7 +21,7 @@ async function main() {
21
21
  program
22
22
  .name('sungen')
23
23
  .description('Deterministic E2E Test Compiler — Gherkin + Selectors → Playwright')
24
- .version('2.6.8');
24
+ .version('2.6.11');
25
25
 
26
26
  // Global options
27
27
  program
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Persist dashboard snapshots under qa/dashboard/history/ and prune to a fixed
3
- * max count (oldest by mtime are deleted). Used by `sungen dashboard` to
4
- * accumulate runs for Trends / Compare views.
2
+ * Persist dashboard snapshots under qa/dashboard/history/. Files are never
3
+ * deleted — `max` only controls how many are *included* in dashboard stats /
4
+ * payload. Older files beyond `max` are retained on disk as `archived` so
5
+ * trends / compare data is recoverable.
5
6
  */
6
7
 
7
8
  import * as fs from 'fs';
@@ -12,8 +13,9 @@ export const DEFAULT_MAX_HISTORY = 20;
12
13
 
13
14
  export interface HistoryWriteResult {
14
15
  written: string; // absolute path written
15
- pruned: string[]; // absolute paths removed
16
- retained: string[]; // absolute paths still on disk (newest → oldest)
16
+ total: number; // total files on disk (after write)
17
+ included: string[]; // newest oldest, capped at max
18
+ archived: string[]; // files beyond max, still on disk
17
19
  }
18
20
 
19
21
  export function historyDir(cwd: string): string {
@@ -21,8 +23,8 @@ export function historyDir(cwd: string): string {
21
23
  }
22
24
 
23
25
  /**
24
- * Write `snapshot` as <runId>.json and prune older files until at most `max`
25
- * files remain. Files are sorted by mtime so manual edits (rare) still work.
26
+ * Write `snapshot` as <runId>.json. Never deletes existing files; `max` only
27
+ * partitions the on-disk list into `included` (newest N) vs `archived`.
26
28
  */
27
29
  export function writeSnapshotToHistory(
28
30
  cwd: string,
@@ -36,10 +38,10 @@ export function writeSnapshotToHistory(
36
38
  const target = path.join(dir, filename);
37
39
  fs.writeFileSync(target, JSON.stringify(snapshot, null, 2), 'utf-8');
38
40
 
39
- const pruned = pruneHistory(dir, max);
40
-
41
- const retained = listHistoryFiles(dir);
42
- return { written: target, pruned, retained };
41
+ const all = listHistoryFiles(dir); // newest first
42
+ const included = all.slice(0, max);
43
+ const archived = all.slice(max);
44
+ return { written: target, total: all.length, included, archived };
43
45
  }
44
46
 
45
47
  /**
@@ -54,11 +56,17 @@ export function listHistoryFiles(dir: string): string[] {
54
56
  }
55
57
 
56
58
  /**
57
- * Read every history JSON, oldest → newest. Skips files that fail to parse.
59
+ * Read history JSONs, oldest → newest. When `limit` is provided, only the
60
+ * newest `limit` files are loaded (older ones stay on disk but are skipped).
61
+ * Skips files that fail to parse.
58
62
  */
59
- export function readHistory(cwd: string): DashboardSnapshot[] {
63
+ export function readHistory(cwd: string, limit?: number): DashboardSnapshot[] {
60
64
  const dir = historyDir(cwd);
61
- const files = listHistoryFiles(dir).reverse(); // oldest → newest
65
+ let files = listHistoryFiles(dir); // newest first
66
+ if (typeof limit === 'number' && limit >= 0) {
67
+ files = files.slice(0, limit);
68
+ }
69
+ files = files.reverse(); // oldest → newest
62
70
  const out: DashboardSnapshot[] = [];
63
71
  for (const f of files) {
64
72
  try {
@@ -70,17 +78,3 @@ export function readHistory(cwd: string): DashboardSnapshot[] {
70
78
  }
71
79
  return out;
72
80
  }
73
-
74
- /**
75
- * Delete oldest files in `dir` until at most `max` remain.
76
- * Returns the absolute paths removed.
77
- */
78
- function pruneHistory(dir: string, max: number): string[] {
79
- const files = listHistoryFiles(dir); // newest first
80
- if (files.length <= max) return [];
81
- const toRemove = files.slice(max);
82
- for (const f of toRemove) {
83
- try { fs.unlinkSync(f); } catch { /* ignore */ }
84
- }
85
- return toRemove;
86
- }
@@ -42,8 +42,12 @@ export function resolveTemplatePath(): string {
42
42
  );
43
43
  }
44
44
 
45
- export function buildPayload(cwd: string, current: DashboardSnapshot): DashboardPayload {
46
- const all = readHistory(cwd);
45
+ export function buildPayload(
46
+ cwd: string,
47
+ current: DashboardSnapshot,
48
+ historyLimit?: number,
49
+ ): DashboardPayload {
50
+ const all = readHistory(cwd, historyLimit);
47
51
  // Exclude `current` from history if it's been written there already.
48
52
  const history = all.filter((s) => s.runId !== current.runId);
49
53
  return { current, history };
@@ -14,6 +14,7 @@
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import { execSync } from 'child_process';
17
+ import { parse as parseYaml } from 'yaml';
17
18
  import { parseFeatureMetadata } from '../exporters/feature-parser';
18
19
  import { parseSpecFile } from '../exporters/spec-parser';
19
20
  import { loadTestData } from '../exporters/test-data-resolver';
@@ -99,11 +100,20 @@ function buildOneScreen(
99
100
  const label = target.isFlow ? `flow/${target.name}` : target.name;
100
101
  const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : undefined;
101
102
 
103
+ // Screens declare their URL inline in the .feature ("Path: /awards"). Flows
104
+ // don't have one there, so fall back to the first `type: 'page'` entry in
105
+ // the flow's selectors YAML — that's where the entry point is recorded.
106
+ let featurePath = feature.featurePath;
107
+ if (!featurePath && target.isFlow) {
108
+ const selectorsFile = path.join(base, 'selectors', `${target.name}.yaml`);
109
+ featurePath = readPagePathFromSelectors(selectorsFile);
110
+ }
111
+
102
112
  return buildScreenSnapshot({
103
113
  screen: label,
104
114
  isFlow: target.isFlow,
105
115
  featureName: feature.featureName,
106
- featurePath: feature.featurePath,
116
+ featurePath,
107
117
  specLink,
108
118
  merged,
109
119
  testData,
@@ -125,7 +135,7 @@ function aggregateSummary(screens: ScreenSnapshot[]): AggregateSummary {
125
135
  na: 0,
126
136
  notCompiled: 0,
127
137
  passRate: 0,
128
- byPriority: { Critical: 0, High: 0, Normal: 0, Low: 0 },
138
+ byPriority: { High: 0, Normal: 0, Low: 0 },
129
139
  byCategory: { Accessing: 0, GUI: 0, Function: 0 },
130
140
  byType: { Auto: 0, Manual: 0, 'Not compiled': 0 },
131
141
  };
@@ -269,5 +279,29 @@ function collectEnvironment(cwd: string, env: EnvironmentInfo) {
269
279
  };
270
280
  }
271
281
 
282
+ /**
283
+ * Read the entry-point path for a flow from its selectors YAML. We pick the
284
+ * first selector whose `type` is `page` — that's the convention sungen uses
285
+ * for the page-selector block in flow YAML files. Returns undefined when the
286
+ * file is missing or has no page entry.
287
+ */
288
+ function readPagePathFromSelectors(selectorsFile: string): string | undefined {
289
+ if (!fs.existsSync(selectorsFile)) return undefined;
290
+ try {
291
+ const raw = fs.readFileSync(selectorsFile, 'utf-8');
292
+ const parsed = parseYaml(raw);
293
+ if (!parsed || typeof parsed !== 'object') return undefined;
294
+ for (const value of Object.values(parsed as Record<string, unknown>)) {
295
+ if (value && typeof value === 'object' && (value as { type?: string }).type === 'page') {
296
+ const v = (value as { value?: unknown }).value;
297
+ if (typeof v === 'string' && v.length > 0) return v;
298
+ }
299
+ }
300
+ } catch {
301
+ // Malformed YAML — fall through to undefined.
302
+ }
303
+ return undefined;
304
+ }
305
+
272
306
  // Helper for ScenarioSnapshot tree filters in UI (re-exported for callers)
273
307
  export type { ScenarioSnapshot };