@sun-asterisk/sungen 2.6.1 → 2.6.2

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 (50) hide show
  1. package/dist/cli/commands/dashboard.d.ts +10 -0
  2. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  3. package/dist/cli/commands/dashboard.js +171 -0
  4. package/dist/cli/commands/dashboard.js.map +1 -0
  5. package/dist/cli/index.js +4 -2
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/dashboard/history-store.d.ts +27 -0
  8. package/dist/dashboard/history-store.d.ts.map +1 -0
  9. package/dist/dashboard/history-store.js +112 -0
  10. package/dist/dashboard/history-store.js.map +1 -0
  11. package/dist/dashboard/html-renderer.d.ts +30 -0
  12. package/dist/dashboard/html-renderer.d.ts.map +1 -0
  13. package/dist/dashboard/html-renderer.js +111 -0
  14. package/dist/dashboard/html-renderer.js.map +1 -0
  15. package/dist/dashboard/snapshot-builder.d.ts +30 -0
  16. package/dist/dashboard/snapshot-builder.d.ts.map +1 -0
  17. package/dist/dashboard/snapshot-builder.js +263 -0
  18. package/dist/dashboard/snapshot-builder.js.map +1 -0
  19. package/dist/dashboard/templates/index.html +287 -0
  20. package/dist/dashboard/types.d.ts +122 -0
  21. package/dist/dashboard/types.d.ts.map +1 -0
  22. package/dist/dashboard/types.js +11 -0
  23. package/dist/dashboard/types.js.map +1 -0
  24. package/dist/exporters/json-exporter.d.ts +25 -0
  25. package/dist/exporters/json-exporter.d.ts.map +1 -0
  26. package/dist/exporters/json-exporter.js +135 -0
  27. package/dist/exporters/json-exporter.js.map +1 -0
  28. package/dist/exporters/playwright-report-parser.d.ts +2 -1
  29. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  30. package/dist/exporters/playwright-report-parser.js +12 -5
  31. package/dist/exporters/playwright-report-parser.js.map +1 -1
  32. package/dist/exporters/spec-parser.d.ts.map +1 -1
  33. package/dist/exporters/spec-parser.js +8 -3
  34. package/dist/exporters/spec-parser.js.map +1 -1
  35. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  36. package/dist/orchestrator/templates/playwright.config.js +9 -1
  37. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  38. package/dist/orchestrator/templates/playwright.config.ts +11 -1
  39. package/package.json +4 -3
  40. package/src/cli/commands/dashboard.ts +158 -0
  41. package/src/cli/index.ts +4 -2
  42. package/src/dashboard/history-store.ts +86 -0
  43. package/src/dashboard/html-renderer.ts +90 -0
  44. package/src/dashboard/snapshot-builder.ts +273 -0
  45. package/src/dashboard/templates/index.html +287 -0
  46. package/src/dashboard/types.ts +148 -0
  47. package/src/exporters/json-exporter.ts +162 -0
  48. package/src/exporters/playwright-report-parser.ts +12 -5
  49. package/src/exporters/spec-parser.ts +8 -3
  50. package/src/orchestrator/templates/playwright.config.ts +11 -1
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Schemas for the sungen dashboard snapshot.
3
+ *
4
+ * A snapshot is a self-describing JSON document that powers a single render
5
+ * of qa/dashboard/index.html. Past snapshots live in qa/dashboard/history/.
6
+ */
7
+
8
+ export const SNAPSHOT_VERSION = 1 as const;
9
+
10
+ export interface DashboardSnapshot {
11
+ version: typeof SNAPSHOT_VERSION;
12
+ runId: string; // ISO-ish: 2026-04-28T10-32-15Z (filename-safe)
13
+ generatedAt: string; // full ISO timestamp
14
+ environment: SnapshotEnvironment;
15
+ summary: AggregateSummary;
16
+ screens: ScreenSnapshot[];
17
+ }
18
+
19
+ export interface SnapshotEnvironment {
20
+ baseURL: string;
21
+ projectName: string;
22
+ executor: string;
23
+ sungenVersion: string;
24
+ gitBranch?: string;
25
+ gitSha?: string;
26
+ }
27
+
28
+ export interface AggregateSummary {
29
+ total: number;
30
+ passed: number;
31
+ failed: number;
32
+ pending: number;
33
+ na: number;
34
+ notCompiled: number;
35
+ passRate: number; // 0..1, only counting executed (passed+failed)
36
+ byPriority: Record<string, number>; // Critical/High/Normal/Low
37
+ byCategory: Record<string, number>; // Accessing/GUI/Function
38
+ byType: Record<string, number>; // Auto/Manual/Not compiled
39
+ }
40
+
41
+ export interface ScreenSnapshot {
42
+ name: string; // "kudos" or "flow/checkout"
43
+ isFlow: boolean;
44
+ featureName: string;
45
+ featurePath?: string;
46
+ specLink?: string; // relative path to requirements/spec.md
47
+ summary: ScreenSummaryStats;
48
+ scenarios: ScenarioSnapshot[];
49
+ }
50
+
51
+ export interface ScreenSummaryStats {
52
+ total: number;
53
+ passed: number;
54
+ failed: number;
55
+ pending: number;
56
+ na: number;
57
+ notCompiled: number;
58
+ passRate: number;
59
+ }
60
+
61
+ export interface ScenarioSnapshot {
62
+ tcId: string; // KUDOS-UI-001
63
+ category1: string; // scenario name (VP-prefix stripped)
64
+ category2: string; // Accessing | GUI | Function
65
+ category3: string; // feature name
66
+ category4: string; // screen name
67
+ vpId?: string; // VP-UI-001 if present
68
+ priority: 'Critical' | 'High' | 'Normal' | 'Low';
69
+ type: 'Auto' | 'Manual' | 'Not compiled';
70
+ status: 'Passed' | 'Failed' | 'Pending' | 'N/A';
71
+ tags: string[];
72
+ precondition: string;
73
+ testData: string;
74
+ steps: string[]; // numbered, one entry per step
75
+ expectedResults: string[];
76
+ duration?: number; // ms
77
+ errorMessage?: string;
78
+ tracePath?: string;
79
+ executedDate?: string; // dd/mm/yyyy
80
+ testEnvironment?: string;
81
+ testExecutor?: string;
82
+ }
83
+
84
+ // ----------------------------------------------------------------------------
85
+ // Diff schemas (Phase 2 will populate these in the dashboard UI from history)
86
+ // ----------------------------------------------------------------------------
87
+
88
+ export interface SnapshotDiff {
89
+ baseRunId: string;
90
+ headRunId: string;
91
+ summary: DiffSummary;
92
+ screens: ScreenDiff[];
93
+ tests: TestDiff[];
94
+ }
95
+
96
+ export interface DiffSummary {
97
+ added: number;
98
+ removed: number;
99
+ newlyPassing: number;
100
+ newlyFailing: number;
101
+ statusChanged: number;
102
+ priorityChanged: number;
103
+ totalDelta: number; // head.total - base.total
104
+ passRateDelta: number; // head - base
105
+ }
106
+
107
+ export interface ScreenDiff {
108
+ name: string;
109
+ totalDelta: number;
110
+ passedDelta: number;
111
+ failedDelta: number;
112
+ passRateDelta: number;
113
+ isNew: boolean;
114
+ isRemoved: boolean;
115
+ }
116
+
117
+ export interface TestDiff {
118
+ tcId: string;
119
+ screen: string;
120
+ changeType:
121
+ | 'added'
122
+ | 'removed'
123
+ | 'newly_passing'
124
+ | 'newly_failing'
125
+ | 'status_changed'
126
+ | 'priority_changed'
127
+ | 'unchanged';
128
+ baseStatus?: ScenarioSnapshot['status'];
129
+ headStatus?: ScenarioSnapshot['status'];
130
+ basePriority?: ScenarioSnapshot['priority'];
131
+ headPriority?: ScenarioSnapshot['priority'];
132
+ category1: string;
133
+ }
134
+
135
+ // ----------------------------------------------------------------------------
136
+ // Wire-format embedded into the HTML template
137
+ // ----------------------------------------------------------------------------
138
+
139
+ /**
140
+ * The single payload injected into qa/dashboard/index.html at build time.
141
+ * Holds the current snapshot plus all retained history runs (max 10).
142
+ *
143
+ * The dashboard UI reads `window.__SUNGEN_DASHBOARD__` at boot.
144
+ */
145
+ export interface DashboardPayload {
146
+ current: DashboardSnapshot;
147
+ history: DashboardSnapshot[]; // oldest → newest, current excluded
148
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Convert merged Gherkin/spec/results data → structured JSON for the dashboard.
3
+ *
4
+ * Mirrors csv-exporter's buildTestCaseRows / buildSummary, but emits the
5
+ * richer ScenarioSnapshot / ScreenSnapshot shape used by the dashboard UI.
6
+ */
7
+
8
+ import { MergedScenario } from './scenario-merger';
9
+ import {
10
+ extractAuthRole,
11
+ extractPriority,
12
+ extractTestcaseType,
13
+ generateTcId,
14
+ mapVpToCategory2,
15
+ splitVpAndName,
16
+ } from './feature-parser';
17
+ import { formatPrecondition, cleanStepLine } from './step-formatter';
18
+ import { formatTestData } from './test-data-resolver';
19
+ import {
20
+ formatExecutedDate,
21
+ statusToTestResult,
22
+ } from './playwright-report-parser';
23
+ import { EnvironmentInfo, PlaywrightResult } from './types';
24
+ import {
25
+ ScenarioSnapshot,
26
+ ScreenSnapshot,
27
+ ScreenSummaryStats,
28
+ } from '../dashboard/types';
29
+
30
+ export interface BuildScreenSnapshotInput {
31
+ screen: string;
32
+ isFlow: boolean;
33
+ featureName: string;
34
+ featurePath?: string;
35
+ specLink?: string;
36
+ merged: MergedScenario[];
37
+ testData: Record<string, string>;
38
+ results: Map<string, PlaywrightResult> | null;
39
+ env: EnvironmentInfo;
40
+ }
41
+
42
+ /**
43
+ * Build a ScreenSnapshot from merged scenarios.
44
+ */
45
+ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnapshot {
46
+ const scenarios: ScenarioSnapshot[] = [];
47
+ let fallbackIndex = 1;
48
+
49
+ for (const m of input.merged) {
50
+ const { vpId, category1 } = splitVpAndName(m.feature.name);
51
+ const tcId = generateTcId(input.screen, vpId, fallbackIndex);
52
+ if (!vpId) fallbackIndex++;
53
+
54
+ const category2 = mapVpToCategory2(vpId);
55
+ const priority = extractPriority(m.feature.tags) as ScenarioSnapshot['priority'];
56
+ const baseType = extractTestcaseType(m.feature.tags);
57
+ const authRole = extractAuthRole(m.feature.tags);
58
+
59
+ let stepsRaw: string[];
60
+ let expectedRaw: string[];
61
+ let givenForPrecondition: string[];
62
+
63
+ if (m.spec) {
64
+ givenForPrecondition = m.spec.precondition;
65
+ stepsRaw = m.spec.steps;
66
+ expectedRaw = m.spec.expectations;
67
+ } else {
68
+ givenForPrecondition = m.feature.rawGivenSteps;
69
+ stepsRaw = m.feature.rawWhenSteps;
70
+ expectedRaw = m.feature.rawThenSteps;
71
+ }
72
+
73
+ const precondition = formatPrecondition(authRole, givenForPrecondition);
74
+ const steps = stepsRaw.map(cleanStepLine).filter(Boolean);
75
+ const expectedResults = expectedRaw.map(cleanStepLine).filter(Boolean);
76
+ const testDataStr = formatTestData(m.feature.referencedVars, input.testData);
77
+
78
+ let result: PlaywrightResult | undefined;
79
+ if (input.results && m.spec) {
80
+ result = input.results.get(m.spec.testTitle);
81
+ }
82
+
83
+ let status: ScenarioSnapshot['status'];
84
+ let executedDate: string | undefined;
85
+ let errorMessage: string | undefined;
86
+ let tracePath: string | undefined;
87
+ let testEnvironment: string | undefined;
88
+ let testExecutor: string | undefined;
89
+ let type: ScenarioSnapshot['type'];
90
+
91
+ if (!m.spec) {
92
+ type = baseType === 'Manual' ? 'Manual' : 'Not compiled';
93
+ status = baseType === 'Manual' ? 'Pending' : 'N/A';
94
+ } else {
95
+ type = baseType;
96
+ if (!result) {
97
+ status = 'Pending';
98
+ } else {
99
+ status = statusToTestResult(result.status) as ScenarioSnapshot['status'];
100
+ executedDate = formatExecutedDate(result.startTime);
101
+ if (result.error) errorMessage = String(result.error).split('\n')[0].trim();
102
+ if (result.tracePath) tracePath = result.tracePath;
103
+ testEnvironment = `${input.env.baseURL} (${input.env.projectName})`;
104
+ testExecutor = input.env.executor;
105
+ }
106
+ }
107
+
108
+ scenarios.push({
109
+ tcId,
110
+ category1,
111
+ category2,
112
+ category3: input.featureName,
113
+ category4: input.screen,
114
+ vpId,
115
+ priority,
116
+ type,
117
+ status,
118
+ tags: m.feature.tags,
119
+ precondition,
120
+ testData: testDataStr,
121
+ steps,
122
+ expectedResults,
123
+ executedDate,
124
+ errorMessage,
125
+ tracePath,
126
+ testEnvironment,
127
+ testExecutor,
128
+ });
129
+ }
130
+
131
+ return {
132
+ name: input.screen,
133
+ isFlow: input.isFlow,
134
+ featureName: input.featureName,
135
+ featurePath: input.featurePath,
136
+ specLink: input.specLink,
137
+ summary: summarizeScreen(scenarios),
138
+ scenarios,
139
+ };
140
+ }
141
+
142
+ function summarizeScreen(scenarios: ScenarioSnapshot[]): ScreenSummaryStats {
143
+ const stats: ScreenSummaryStats = {
144
+ total: scenarios.length,
145
+ passed: 0,
146
+ failed: 0,
147
+ pending: 0,
148
+ na: 0,
149
+ notCompiled: 0,
150
+ passRate: 0,
151
+ };
152
+ for (const s of scenarios) {
153
+ if (s.status === 'Passed') stats.passed++;
154
+ else if (s.status === 'Failed') stats.failed++;
155
+ else if (s.status === 'Pending') stats.pending++;
156
+ else if (s.status === 'N/A') stats.na++;
157
+ if (s.type === 'Not compiled') stats.notCompiled++;
158
+ }
159
+ const executed = stats.passed + stats.failed;
160
+ stats.passRate = executed > 0 ? stats.passed / executed : 0;
161
+ return stats;
162
+ }
@@ -74,7 +74,11 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
74
74
  const result = new Map<string, PlaywrightResult>();
75
75
  for (const [fullTitle, spec] of specMap.entries()) {
76
76
  const test = spec.tests?.[0];
77
- const res = test?.results?.[0];
77
+ // With retries enabled, take the LAST attempt — that's the final outcome.
78
+ // A flaky test (failed once, passed retry) shows as "passed" in the dashboard
79
+ // because the user-visible end state is "passed".
80
+ const results = test?.results || [];
81
+ const res = results[results.length - 1];
78
82
  const status = normalizeStatus(res?.status);
79
83
  const errorMsg = res?.error?.message || '';
80
84
  const trace = res?.attachments?.find((a) => a.name === 'trace' || a.contentType === 'application/zip')?.path;
@@ -141,15 +145,18 @@ export function statusToTestResult(status: PlaywrightResult['status']): string {
141
145
  }
142
146
 
143
147
  /**
144
- * Format startTime (ISO string) → "dd/mm/yyyy".
148
+ * Format startTime (ISO string) → "dd/mm/yyyy" in Vietnam time (UTC+7),
149
+ * so reports stay consistent regardless of where Playwright was executed.
145
150
  */
146
151
  export function formatExecutedDate(startTime: string | undefined): string {
147
152
  if (!startTime) return '';
148
153
  const d = new Date(startTime);
149
154
  if (isNaN(d.getTime())) return '';
150
- const dd = String(d.getDate()).padStart(2, '0');
151
- const mm = String(d.getMonth() + 1).padStart(2, '0');
152
- const yyyy = d.getFullYear();
155
+ const VN_OFFSET_MS = 7 * 60 * 60 * 1000;
156
+ const shifted = new Date(d.getTime() + VN_OFFSET_MS);
157
+ const dd = String(shifted.getUTCDate()).padStart(2, '0');
158
+ const mm = String(shifted.getUTCMonth() + 1).padStart(2, '0');
159
+ const yyyy = shifted.getUTCFullYear();
153
160
  return `${dd}/${mm}/${yyyy}`;
154
161
  }
155
162
 
@@ -15,13 +15,18 @@ function extractTestBlock(content: string, startIdx: number): {
15
15
  body: string;
16
16
  endIdx: number;
17
17
  } | null {
18
- // Match test('title', async ({ page }) => {
19
- const testRegex = /test\s*\(\s*['"]([^'"]+)['"]\s*,\s*async\s*\([^)]*\)\s*=>\s*\{/g;
18
+ // Match both legacy and tag-pass-through forms:
19
+ // test('title', async ({ page }) => {
20
+ // test('title', { tag: [...] }, async ({ page }) => {
21
+ // Backreference \1 lets the inner title contain the opposite quote type
22
+ // (e.g. test('Footer "X" link', ...) — common when scenarios cite UI labels).
23
+ const testRegex = /test\s*\(\s*(['"])((?:(?!\1).)+)\1\s*,\s*(?:\{[^}]*\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
20
24
  testRegex.lastIndex = startIdx;
21
25
  const match = testRegex.exec(content);
22
26
  if (!match) return null;
23
27
 
24
- const title = match[1];
28
+ // [1] = outer quote char, [2] = title
29
+ const title = match[2];
25
30
  const bodyStart = match.index + match[0].length;
26
31
 
27
32
  // Find matching closing brace (accounting for nested braces)
@@ -28,7 +28,14 @@ export default defineConfig({
28
28
  /* Output file path is controlled by PLAYWRIGHT_JSON_OUTPUT_NAME env var for per-screen isolation. */
29
29
  reporter: [
30
30
  ['html'],
31
- ['json', { outputFile: process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || 'test-results/results.json' }],
31
+ [
32
+ 'json',
33
+ {
34
+ outputFile:
35
+ process.env.PLAYWRIGHT_JSON_OUTPUT_NAME ||
36
+ 'test-results/results.json',
37
+ },
38
+ ],
32
39
  ],
33
40
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
34
41
  use: {
@@ -40,6 +47,9 @@ export default defineConfig({
40
47
 
41
48
  /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
42
49
  trace: 'on-first-retry',
50
+
51
+ /* Capture screenshot after each test failure. */
52
+ screenshot: 'only-on-failure',
43
53
  },
44
54
 
45
55
  /* Configure projects for major browsers */