@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.
- package/dist/cli/commands/dashboard.d.ts +10 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -0
- package/dist/cli/commands/dashboard.js +171 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/index.js +4 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/history-store.d.ts +27 -0
- package/dist/dashboard/history-store.d.ts.map +1 -0
- package/dist/dashboard/history-store.js +112 -0
- package/dist/dashboard/history-store.js.map +1 -0
- package/dist/dashboard/html-renderer.d.ts +30 -0
- package/dist/dashboard/html-renderer.d.ts.map +1 -0
- package/dist/dashboard/html-renderer.js +111 -0
- package/dist/dashboard/html-renderer.js.map +1 -0
- package/dist/dashboard/snapshot-builder.d.ts +30 -0
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -0
- package/dist/dashboard/snapshot-builder.js +263 -0
- package/dist/dashboard/snapshot-builder.js.map +1 -0
- package/dist/dashboard/templates/index.html +287 -0
- package/dist/dashboard/types.d.ts +122 -0
- package/dist/dashboard/types.d.ts.map +1 -0
- package/dist/dashboard/types.js +11 -0
- package/dist/dashboard/types.js.map +1 -0
- package/dist/exporters/json-exporter.d.ts +25 -0
- package/dist/exporters/json-exporter.d.ts.map +1 -0
- package/dist/exporters/json-exporter.js +135 -0
- package/dist/exporters/json-exporter.js.map +1 -0
- package/dist/exporters/playwright-report-parser.d.ts +2 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +12 -5
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +8 -3
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
- package/dist/orchestrator/templates/playwright.config.js +9 -1
- package/dist/orchestrator/templates/playwright.config.js.map +1 -1
- package/dist/orchestrator/templates/playwright.config.ts +11 -1
- package/package.json +4 -3
- package/src/cli/commands/dashboard.ts +158 -0
- package/src/cli/index.ts +4 -2
- package/src/dashboard/history-store.ts +86 -0
- package/src/dashboard/html-renderer.ts +90 -0
- package/src/dashboard/snapshot-builder.ts +273 -0
- package/src/dashboard/templates/index.html +287 -0
- package/src/dashboard/types.ts +148 -0
- package/src/exporters/json-exporter.ts +162 -0
- package/src/exporters/playwright-report-parser.ts +12 -5
- package/src/exporters/spec-parser.ts +8 -3
- 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
|
-
|
|
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
|
|
151
|
-
const
|
|
152
|
-
const
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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 */
|