@ai-qa/workflow 2.0.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.
- package/.github/agents/playwright-test-generator.agent.md +33 -0
- package/.github/agents/playwright-test-healer.agent.md +36 -0
- package/.github/agents/playwright-test-planner.agent.md +44 -0
- package/.opencode/agents/qa-generator.md +19 -0
- package/.opencode/agents/qa-healer.md +25 -0
- package/.opencode/agents/qa-planner.md +20 -0
- package/.qa-workflow.json +22 -0
- package/README.md +365 -0
- package/ai-qa-workflow.js +330 -0
- package/cli.js +7 -0
- package/docs/application-context.md +20 -0
- package/install.js +303 -0
- package/opencode.json +31 -0
- package/package.json +30 -0
- package/prompts/QAe2eprompt.md +513 -0
- package/prompts/general_prompt.md +13 -0
- package/qa-dashboard/.env +3 -0
- package/qa-dashboard/app.js +46 -0
- package/qa-dashboard/package.json +18 -0
- package/qa-dashboard/public/css/style.css +266 -0
- package/qa-dashboard/public/js/main.js +6 -0
- package/qa-dashboard/routes/analytics.js +52 -0
- package/qa-dashboard/routes/export.js +153 -0
- package/qa-dashboard/routes/index.js +10 -0
- package/qa-dashboard/routes/projects.js +92 -0
- package/qa-dashboard/routes/runs.js +66 -0
- package/qa-dashboard/routes/stories.js +101 -0
- package/qa-dashboard/routes/test-data.js +82 -0
- package/qa-dashboard/services/cli-bridge.js +143 -0
- package/qa-dashboard/services/project-manager.js +61 -0
- package/qa-dashboard/views/analytics.ejs +188 -0
- package/qa-dashboard/views/error.ejs +8 -0
- package/qa-dashboard/views/index.ejs +83 -0
- package/qa-dashboard/views/layouts/main.ejs +28 -0
- package/qa-dashboard/views/project-add.ejs +23 -0
- package/qa-dashboard/views/project.ejs +97 -0
- package/qa-dashboard/views/projects.ejs +29 -0
- package/qa-dashboard/views/run.ejs +84 -0
- package/qa-dashboard/views/runs.ejs +64 -0
- package/qa-dashboard/views/stories.ejs +53 -0
- package/qa-dashboard/views/story.ejs +63 -0
- package/qa-dashboard/views/test-data.ejs +117 -0
- package/scripts/executor.js +142 -0
- package/scripts/generator.js +130 -0
- package/scripts/healer.js +207 -0
- package/scripts/planner.js +142 -0
- package/scripts/reporter.js +190 -0
- package/scripts/utils.js +244 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #0f1117;
|
|
4
|
+
--surface: #1a1d27;
|
|
5
|
+
--surface2: #232734;
|
|
6
|
+
--border: #2d3140;
|
|
7
|
+
--text: #e4e6f0;
|
|
8
|
+
--text-muted: #8b8fa3;
|
|
9
|
+
--primary: #6366f1;
|
|
10
|
+
--primary-hover: #818cf8;
|
|
11
|
+
--success: #22c55e;
|
|
12
|
+
--danger: #ef4444;
|
|
13
|
+
--warning: #f59e0b;
|
|
14
|
+
--radius: 8px;
|
|
15
|
+
}
|
|
16
|
+
body {
|
|
17
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
color: var(--text);
|
|
20
|
+
line-height: 1.6;
|
|
21
|
+
}
|
|
22
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
|
|
23
|
+
|
|
24
|
+
/* Navbar */
|
|
25
|
+
.navbar {
|
|
26
|
+
background: var(--surface);
|
|
27
|
+
border-bottom: 1px solid var(--border);
|
|
28
|
+
padding: 0 1.5rem;
|
|
29
|
+
position: sticky; top: 0; z-index: 100;
|
|
30
|
+
}
|
|
31
|
+
.nav-inner {
|
|
32
|
+
max-width: 1200px; margin: 0 auto;
|
|
33
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
34
|
+
height: 56px;
|
|
35
|
+
}
|
|
36
|
+
.nav-brand {
|
|
37
|
+
font-size: 1.2rem; font-weight: 700;
|
|
38
|
+
color: var(--primary); text-decoration: none;
|
|
39
|
+
}
|
|
40
|
+
.nav-links { display: flex; gap: 1.5rem; }
|
|
41
|
+
.nav-links a {
|
|
42
|
+
color: var(--text-muted); text-decoration: none;
|
|
43
|
+
font-size: 0.9rem; transition: color 0.2s;
|
|
44
|
+
}
|
|
45
|
+
.nav-links a:hover { color: var(--text); }
|
|
46
|
+
|
|
47
|
+
/* Hero */
|
|
48
|
+
.hero {
|
|
49
|
+
text-align: center; padding: 3rem 0 2rem;
|
|
50
|
+
}
|
|
51
|
+
.hero h1 { font-size: 2.5rem; font-weight: 800; margin-bottom: 0.5rem; }
|
|
52
|
+
.hero p { color: var(--text-muted); font-size: 1.1rem; }
|
|
53
|
+
|
|
54
|
+
/* Stats */
|
|
55
|
+
.stats-row { display: flex; gap: 1rem; justify-content: center; margin: 2rem 0; }
|
|
56
|
+
.stat-card {
|
|
57
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
58
|
+
border-radius: var(--radius); padding: 1.5rem 2rem; text-align: center;
|
|
59
|
+
min-width: 150px;
|
|
60
|
+
}
|
|
61
|
+
.stat-number { font-size: 2rem; font-weight: 700; color: var(--primary); }
|
|
62
|
+
.stat-label { color: var(--text-muted); font-size: 0.85rem; margin-top: 0.25rem; }
|
|
63
|
+
|
|
64
|
+
/* Cards */
|
|
65
|
+
.card {
|
|
66
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
67
|
+
border-radius: var(--radius); padding: 1.5rem;
|
|
68
|
+
margin-bottom: 1rem;
|
|
69
|
+
}
|
|
70
|
+
.card h3 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--primary); }
|
|
71
|
+
|
|
72
|
+
/* Pipeline Flow */
|
|
73
|
+
.pipeline-flow { margin: 2rem 0; }
|
|
74
|
+
.pipeline-flow h2 { margin-bottom: 1rem; }
|
|
75
|
+
.flow-steps {
|
|
76
|
+
display: flex; align-items: center; gap: 0.75rem;
|
|
77
|
+
flex-wrap: wrap; justify-content: center;
|
|
78
|
+
}
|
|
79
|
+
.flow-step {
|
|
80
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
81
|
+
border-radius: var(--radius); padding: 1rem 1.25rem;
|
|
82
|
+
text-align: center; min-width: 110px;
|
|
83
|
+
}
|
|
84
|
+
.step-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
|
|
85
|
+
.step-name { font-weight: 600; font-size: 0.85rem; }
|
|
86
|
+
.step-desc { color: var(--text-muted); font-size: 0.75rem; }
|
|
87
|
+
.flow-arrow { color: var(--text-muted); font-size: 1.25rem; }
|
|
88
|
+
|
|
89
|
+
/* Grid */
|
|
90
|
+
.project-grid, .row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
91
|
+
.col { flex: 1; min-width: 200px; }
|
|
92
|
+
.col-30 { flex: 0 0 30%; }
|
|
93
|
+
.col-40 { flex: 0 0 38%; }
|
|
94
|
+
.col-60 { flex: 0 0 58%; }
|
|
95
|
+
.col-70 { flex: 0 0 68%; }
|
|
96
|
+
|
|
97
|
+
/* Project Card */
|
|
98
|
+
.project-card {
|
|
99
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
100
|
+
border-radius: var(--radius); padding: 1.25rem;
|
|
101
|
+
text-decoration: none; color: var(--text);
|
|
102
|
+
display: block; transition: border-color 0.2s;
|
|
103
|
+
min-width: 250px; flex: 1;
|
|
104
|
+
}
|
|
105
|
+
.project-card:hover { border-color: var(--primary); }
|
|
106
|
+
.project-name { font-weight: 600; font-size: 1.1rem; }
|
|
107
|
+
.project-path { color: var(--text-muted); font-size: 0.8rem; word-break: break-all; }
|
|
108
|
+
.project-meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.5rem; }
|
|
109
|
+
.project-actions { margin-top: 0.75rem; display: flex; gap: 0.5rem; }
|
|
110
|
+
|
|
111
|
+
/* Page Header */
|
|
112
|
+
.page-header {
|
|
113
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
114
|
+
margin-bottom: 1.5rem; flex-wrap: wrap; gap: 0.75rem;
|
|
115
|
+
}
|
|
116
|
+
.page-header h1 { font-size: 1.5rem; }
|
|
117
|
+
.header-actions { display: flex; gap: 0.5rem; }
|
|
118
|
+
|
|
119
|
+
/* Buttons */
|
|
120
|
+
.btn {
|
|
121
|
+
display: inline-block; padding: 0.5rem 1rem;
|
|
122
|
+
border-radius: var(--radius); border: 1px solid var(--border);
|
|
123
|
+
background: var(--surface2); color: var(--text);
|
|
124
|
+
font-size: 0.85rem; cursor: pointer; text-decoration: none;
|
|
125
|
+
transition: all 0.2s;
|
|
126
|
+
}
|
|
127
|
+
.btn:hover { border-color: var(--primary); color: var(--primary); }
|
|
128
|
+
.btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; }
|
|
129
|
+
.btn-primary:hover { background: var(--primary-hover); color: #fff; }
|
|
130
|
+
.btn-sm { padding: 0.35rem 0.75rem; font-size: 0.8rem; }
|
|
131
|
+
.btn-danger { border-color: var(--danger); color: var(--danger); }
|
|
132
|
+
.btn-danger:hover { background: var(--danger); color: #fff; }
|
|
133
|
+
|
|
134
|
+
/* Forms */
|
|
135
|
+
.form { max-width: 500px; }
|
|
136
|
+
.form-group { margin-bottom: 1rem; }
|
|
137
|
+
.form-group label { display: block; margin-bottom: 0.35rem; font-size: 0.9rem; }
|
|
138
|
+
.form-group small { color: var(--text-muted); font-size: 0.8rem; }
|
|
139
|
+
.form-input, .form input[type="text"], .form select {
|
|
140
|
+
width: 100%; padding: 0.6rem 0.75rem;
|
|
141
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
142
|
+
border-radius: var(--radius); color: var(--text); font-size: 0.9rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Badges */
|
|
146
|
+
.badge {
|
|
147
|
+
display: inline-block; padding: 0.2rem 0.6rem;
|
|
148
|
+
border-radius: 100px; font-size: 0.75rem; font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
.badge-project { background: var(--surface2); color: var(--text-muted); border: 1px solid var(--border); }
|
|
151
|
+
.badge-plan { background: #1e3a5f; color: #60a5fa; }
|
|
152
|
+
.badge-spec { background: #14532d; color: #4ade80; }
|
|
153
|
+
.badge-draft { background: #422006; color: #fbbf24; }
|
|
154
|
+
.badge-pass { background: #14532d; color: #4ade80; }
|
|
155
|
+
.badge-fail { background: #450a0a; color: #f87171; }
|
|
156
|
+
|
|
157
|
+
/* Stories */
|
|
158
|
+
.story-list { display: flex; flex-direction: column; gap: 1rem; }
|
|
159
|
+
.story-card {
|
|
160
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
161
|
+
border-radius: var(--radius); padding: 1.25rem;
|
|
162
|
+
}
|
|
163
|
+
.story-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
|
|
164
|
+
.story-title { font-weight: 600; color: var(--primary); text-decoration: none; font-size: 1.05rem; }
|
|
165
|
+
.story-title:hover { text-decoration: underline; }
|
|
166
|
+
.story-badges { display: flex; gap: 0.35rem; }
|
|
167
|
+
.story-preview { color: var(--text-muted); font-size: 0.85rem; }
|
|
168
|
+
.story-preview p { margin: 0.15rem 0; }
|
|
169
|
+
.story-actions { margin-top: 0.75rem; }
|
|
170
|
+
.story-action-output { margin-left: 0.5rem; font-size: 0.85rem; }
|
|
171
|
+
|
|
172
|
+
/* Runs */
|
|
173
|
+
.run-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
174
|
+
.run-card {
|
|
175
|
+
display: flex; align-items: center; gap: 1rem;
|
|
176
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
177
|
+
border-radius: var(--radius); padding: 1rem 1.25rem;
|
|
178
|
+
text-decoration: none; color: var(--text); transition: border-color 0.2s;
|
|
179
|
+
}
|
|
180
|
+
.run-card:hover { border-color: var(--primary); }
|
|
181
|
+
.run-passed { border-left: 3px solid var(--success); }
|
|
182
|
+
.run-failed { border-left: 3px solid var(--danger); }
|
|
183
|
+
.run-icon { font-size: 1.25rem; }
|
|
184
|
+
.run-name { font-weight: 600; font-size: 0.9rem; }
|
|
185
|
+
.run-test { color: var(--text-muted); font-size: 0.8rem; }
|
|
186
|
+
.run-meta { color: var(--text-muted); font-size: 0.75rem; margin-top: 0.2rem; }
|
|
187
|
+
|
|
188
|
+
/* Output */
|
|
189
|
+
.output-box {
|
|
190
|
+
margin-top: 0.75rem; padding: 0.75rem;
|
|
191
|
+
border-radius: var(--radius); font-size: 0.8rem;
|
|
192
|
+
max-height: 300px; overflow-y: auto; white-space: pre-wrap;
|
|
193
|
+
font-family: 'Consolas', 'Courier New', monospace;
|
|
194
|
+
}
|
|
195
|
+
.output-running { background: #1e1b4b; color: #a5b4fc; border: 1px solid #3730a3; }
|
|
196
|
+
.output-success { background: #052e16; color: #86efac; border: 1px solid #166534; }
|
|
197
|
+
.output-error { background: #450a0a; color: #fca5a5; border: 1px solid #991b1b; }
|
|
198
|
+
|
|
199
|
+
/* Action buttons */
|
|
200
|
+
.action-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
201
|
+
.action-buttons-vertical { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
202
|
+
|
|
203
|
+
/* Info table */
|
|
204
|
+
.info-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
205
|
+
.info-table td { padding: 0.5rem 0.25rem; border-bottom: 1px solid var(--border); }
|
|
206
|
+
.info-table td:first-child { color: var(--text-muted); width: 40%; }
|
|
207
|
+
|
|
208
|
+
/* Code */
|
|
209
|
+
.code-block {
|
|
210
|
+
background: #0d0f16; border: 1px solid var(--border);
|
|
211
|
+
border-radius: var(--radius); padding: 1rem;
|
|
212
|
+
font-size: 0.78rem; overflow-x: auto; max-height: 400px;
|
|
213
|
+
font-family: 'Consolas', 'Courier New', monospace;
|
|
214
|
+
line-height: 1.5;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Markdown */
|
|
218
|
+
.markdown-content { font-size: 0.85rem; line-height: 1.6; }
|
|
219
|
+
.markdown-content br { margin-bottom: 0.25rem; }
|
|
220
|
+
|
|
221
|
+
/* Heal */
|
|
222
|
+
.heal-attempt {
|
|
223
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
224
|
+
border-radius: var(--radius); padding: 0.5rem 0.75rem;
|
|
225
|
+
margin: 0.35rem 0; font-size: 0.8rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Fail list */
|
|
229
|
+
.fail-list { list-style: none; }
|
|
230
|
+
.fail-list li {
|
|
231
|
+
background: #450a0a; border: 1px solid #991b1b;
|
|
232
|
+
border-radius: var(--radius); padding: 0.75rem;
|
|
233
|
+
margin-bottom: 0.5rem; font-size: 0.85rem;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* Big number */
|
|
237
|
+
.big-number { font-size: 2.5rem; font-weight: 700; color: var(--primary); margin-bottom: 0.5rem; }
|
|
238
|
+
|
|
239
|
+
/* Empty state */
|
|
240
|
+
.empty-state { text-align: center; padding: 4rem 1rem; }
|
|
241
|
+
.empty-state h3 { margin-bottom: 0.5rem; }
|
|
242
|
+
.empty-state p { color: var(--text-muted); margin-bottom: 1rem; }
|
|
243
|
+
|
|
244
|
+
/* Alert */
|
|
245
|
+
.alert { padding: 0.75rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; font-size: 0.85rem; }
|
|
246
|
+
.alert-error { background: #450a0a; border: 1px solid #991b1b; color: #fca5a5; }
|
|
247
|
+
|
|
248
|
+
/* Text colors */
|
|
249
|
+
.text-success { color: var(--success); }
|
|
250
|
+
.text-warning { color: var(--warning); }
|
|
251
|
+
.text-danger { color: var(--danger); }
|
|
252
|
+
|
|
253
|
+
/* Column helpers */
|
|
254
|
+
.col-50 { flex: 0 0 48%; }
|
|
255
|
+
.col-60 { flex: 0 0 58%; }
|
|
256
|
+
.col-40 { flex: 0 0 38%; }
|
|
257
|
+
@media (max-width: 768px) {
|
|
258
|
+
.col-50, .col-60, .col-40 { flex: 0 0 100%; }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Table header styling */
|
|
262
|
+
.info-table th { color: var(--text-muted); font-weight: 600; font-size: 0.75rem; text-transform: uppercase; padding: 0.5rem 0.25rem; border-bottom: 2px solid var(--border); text-align: left; }
|
|
263
|
+
|
|
264
|
+
/* Refresh spinner */
|
|
265
|
+
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.6s linear infinite; }
|
|
266
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pm = require('../services/project-manager');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
router.get('/', (req, res) => {
|
|
8
|
+
const projects = pm.getAll();
|
|
9
|
+
const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
|
|
10
|
+
const project = projects.find(p => p.id === projectId);
|
|
11
|
+
|
|
12
|
+
if (!project || !pm.getBridge(projectId)) {
|
|
13
|
+
return res.render('analytics', { project: null, projects, runs: [], stats: { totalRuns: 0, passedRuns: 0, totalFailures: 0, totalHealed: 0, totalDefects: 0, avgDuration: 0, passRate: 0, healRate: 0 } });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const bridge = pm.getBridge(project.id);
|
|
17
|
+
const runDirs = bridge.getRunDirs();
|
|
18
|
+
|
|
19
|
+
const runs = runDirs.map(d => {
|
|
20
|
+
const result = bridge.getRunResult(d.name);
|
|
21
|
+
const healing = bridge.getHealingReport(d.name);
|
|
22
|
+
return {
|
|
23
|
+
id: d.name,
|
|
24
|
+
timestamp: d.name.replace('run-', '').replace(/-/g, ':').slice(0, 19),
|
|
25
|
+
success: result ? result.success : null,
|
|
26
|
+
duration: result ? result.duration : null,
|
|
27
|
+
total: result && result.failedTests ? result.failedTests.length + (result.passedTests ? result.passedTests.length : 0) : 0,
|
|
28
|
+
failures: result && result.failedTests ? result.failedTests.length : 0,
|
|
29
|
+
healed: healing ? healing.totalHealed || 0 : 0,
|
|
30
|
+
defects: healing && healing.defects ? healing.defects.length : 0,
|
|
31
|
+
testName: result ? result.test : 'N/A',
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const totalRuns = runs.length;
|
|
36
|
+
const passedRuns = runs.filter(r => r.success).length;
|
|
37
|
+
const totalFailures = runs.reduce((s, r) => s + r.failures, 0);
|
|
38
|
+
const totalHealed = runs.reduce((s, r) => s + r.healed, 0);
|
|
39
|
+
const totalDefects = runs.reduce((s, r) => s + r.defects, 0);
|
|
40
|
+
const avgDuration = runs.length > 0 ? (runs.reduce((s, r) => s + (r.duration || 0), 0) / runs.length) : 0;
|
|
41
|
+
const passRate = totalRuns > 0 ? ((passedRuns / totalRuns) * 100).toFixed(1) : 0;
|
|
42
|
+
const healRate = totalFailures > 0 ? ((totalHealed / totalFailures) * 100).toFixed(1) : 0;
|
|
43
|
+
|
|
44
|
+
res.render('analytics', {
|
|
45
|
+
project,
|
|
46
|
+
projects,
|
|
47
|
+
runs: runs.reverse(),
|
|
48
|
+
stats: { totalRuns, passedRuns, totalFailures, totalHealed, totalDefects, avgDuration, passRate, healRate },
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
module.exports = router;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pm = require('../services/project-manager');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
function resolveProject(req) {
|
|
8
|
+
const projects = pm.getAll();
|
|
9
|
+
const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
|
|
10
|
+
return projects.find(p => p.id === projectId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveRunId(runId, bridge) {
|
|
14
|
+
if (runId === 'latest') {
|
|
15
|
+
const dirs = bridge.getRunDirs();
|
|
16
|
+
return dirs.length > 0 ? dirs[0].name : null;
|
|
17
|
+
}
|
|
18
|
+
return runId;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
router.get('/report/:runId', (req, res) => {
|
|
22
|
+
const project = resolveProject(req);
|
|
23
|
+
if (!project) return res.status(404).send('Project not found');
|
|
24
|
+
|
|
25
|
+
const bridge = pm.getBridge(project.id);
|
|
26
|
+
const runId = resolveRunId(req.params.runId, bridge);
|
|
27
|
+
if (!runId) return res.status(404).send('No runs found');
|
|
28
|
+
|
|
29
|
+
const report = bridge.getFinalReport(runId);
|
|
30
|
+
const result = bridge.getRunResult(runId);
|
|
31
|
+
const healing = bridge.getHealingReport(runId);
|
|
32
|
+
|
|
33
|
+
if (!result) return res.status(404).send('Run not found');
|
|
34
|
+
|
|
35
|
+
const html = `<!DOCTYPE html>
|
|
36
|
+
<html><head><meta charset="UTF-8"><title>QA Report - ${runId}</title>
|
|
37
|
+
<style>
|
|
38
|
+
body { font-family: Arial; max-width: 800px; margin: 2rem auto; padding: 1rem; color: #333; }
|
|
39
|
+
h1 { color: #6366f1; border-bottom: 2px solid #6366f1; padding-bottom: 0.5rem; }
|
|
40
|
+
.badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 14px; font-weight: bold; }
|
|
41
|
+
.pass { background: #dcfce7; color: #166534; }
|
|
42
|
+
.fail { background: #fecaca; color: #991b1b; }
|
|
43
|
+
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
|
44
|
+
td, th { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
45
|
+
th { background: #f4f4f5; }
|
|
46
|
+
.section { margin: 2rem 0; }
|
|
47
|
+
.stats { display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
48
|
+
.stat-card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; min-width: 120px; text-align: center; }
|
|
49
|
+
.stat-num { font-size: 24px; font-weight: bold; color: #6366f1; }
|
|
50
|
+
.stat-label { font-size: 12px; color: #666; }
|
|
51
|
+
.footer { margin-top: 2rem; font-size: 12px; color: #999; border-top: 1px solid #eee; padding-top: 1rem; }
|
|
52
|
+
</style></head><body>
|
|
53
|
+
<h1>AI QA Execution Report</h1>
|
|
54
|
+
<p>Run: ${runId} | ${new Date().toLocaleDateString()}</p>
|
|
55
|
+
<span class="badge ${result.success ? 'pass' : 'fail'}">${result.success ? 'PASSED' : 'FAILED'}</span>
|
|
56
|
+
|
|
57
|
+
<div class="section">
|
|
58
|
+
<h2>Summary</h2>
|
|
59
|
+
<div class="stats">
|
|
60
|
+
<div class="stat-card"><div class="stat-num">${result.failedTests ? result.failedTests.length : 0}</div><div class="stat-label">Failures</div></div>
|
|
61
|
+
<div class="stat-card"><div class="stat-num">${healing ? healing.totalHealed || 0 : 0}</div><div class="stat-label">Healed</div></div>
|
|
62
|
+
<div class="stat-card"><div class="stat-num">${healing && healing.defects ? healing.defects.length : 0}</div><div class="stat-label">Defects</div></div>
|
|
63
|
+
<div class="stat-card"><div class="stat-num">${result.duration ? (result.duration / 1000).toFixed(1) + 's' : 'N/A'}</div><div class="stat-label">Duration</div></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
${healing && healing.defects && healing.defects.length > 0 ? `
|
|
68
|
+
<div class="section">
|
|
69
|
+
<h2>Defects</h2>
|
|
70
|
+
<table><tr><th>Test</th><th>Type</th><th>Verdict</th></tr>
|
|
71
|
+
${healing.defects.map(d => `<tr><td>${d.test}</td><td>${d.classification}</td><td>${d.verdict}</td></tr>`).join('')}
|
|
72
|
+
</table>
|
|
73
|
+
</div>` : ''}
|
|
74
|
+
|
|
75
|
+
<div class="section">
|
|
76
|
+
<h2>Full Report</h2>
|
|
77
|
+
<pre style="background:#f4f4f5;padding:1rem;border-radius:8px;font-size:12px;max-height:400px;overflow:auto;">${(report || result.output || 'No detailed report').substring(0, 5000)}</pre>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="footer">
|
|
81
|
+
Generated by AI QA Pipeline | ${new Date().toISOString()}
|
|
82
|
+
</div>
|
|
83
|
+
</body></html>`;
|
|
84
|
+
|
|
85
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
86
|
+
res.setHeader('Content-Disposition', 'inline');
|
|
87
|
+
res.send(html);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
router.get('/pdf/:runId', (req, res) => {
|
|
91
|
+
const project = resolveProject(req);
|
|
92
|
+
if (!project) return res.status(404).send('Project not found');
|
|
93
|
+
|
|
94
|
+
const bridge = pm.getBridge(project.id);
|
|
95
|
+
const runId = resolveRunId(req.params.runId, bridge);
|
|
96
|
+
if (!runId) return res.status(404).send('No runs found');
|
|
97
|
+
|
|
98
|
+
const result = bridge.getRunResult(runId);
|
|
99
|
+
if (!result) return res.status(404).send('Run not found');
|
|
100
|
+
|
|
101
|
+
const report = bridge.getFinalReport(runId);
|
|
102
|
+
const healing = bridge.getHealingReport(runId);
|
|
103
|
+
|
|
104
|
+
const lines = [];
|
|
105
|
+
lines.push('='.repeat(60));
|
|
106
|
+
lines.push('AI QA EXECUTION REPORT');
|
|
107
|
+
lines.push('='.repeat(60));
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push(`Run ID: ${runId}`);
|
|
110
|
+
lines.push(`Date: ${new Date().toLocaleDateString()}`);
|
|
111
|
+
lines.push(`Status: ${result.success ? 'PASSED' : 'FAILED'}`);
|
|
112
|
+
lines.push(`Duration: ${result.duration ? (result.duration / 1000).toFixed(1) + 's' : 'N/A'}`);
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push('-'.repeat(60));
|
|
115
|
+
lines.push('RESULTS');
|
|
116
|
+
lines.push('-'.repeat(60));
|
|
117
|
+
lines.push(`Failures: ${result.failedTests ? result.failedTests.length : 0}`);
|
|
118
|
+
lines.push(`Healed: ${healing ? healing.totalHealed || 0 : 0}`);
|
|
119
|
+
lines.push(`Defects: ${healing && healing.defects ? healing.defects.length : 0}`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
|
|
122
|
+
if (healing && healing.defects && healing.defects.length > 0) {
|
|
123
|
+
lines.push('-'.repeat(60));
|
|
124
|
+
lines.push('DEFECTS');
|
|
125
|
+
lines.push('-'.repeat(60));
|
|
126
|
+
healing.defects.forEach(d => {
|
|
127
|
+
lines.push(` [${d.verdict}] ${d.test}`);
|
|
128
|
+
lines.push(` Type: ${d.classification}`);
|
|
129
|
+
lines.push(` ${d.error ? d.error.substring(0, 150) : ''}`);
|
|
130
|
+
if (d.screenshot) lines.push(` Screenshot: ${d.screenshot}`);
|
|
131
|
+
lines.push('');
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (report) {
|
|
136
|
+
lines.push('-'.repeat(60));
|
|
137
|
+
lines.push('FULL REPORT');
|
|
138
|
+
lines.push('-'.repeat(60));
|
|
139
|
+
lines.push(report.substring(0, 3000));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('='.repeat(60));
|
|
144
|
+
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
145
|
+
lines.push('='.repeat(60));
|
|
146
|
+
|
|
147
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
148
|
+
res.setHeader('Content-Disposition', `attachment; filename="qa-report-${runId}.txt"`);
|
|
149
|
+
res.setHeader('Content-Description', 'AI QA Test Report');
|
|
150
|
+
res.send(lines.join('\n'));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
module.exports = router;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pm = require('../services/project-manager');
|
|
4
|
+
|
|
5
|
+
router.get('/', (req, res) => {
|
|
6
|
+
const projects = pm.getAll();
|
|
7
|
+
res.render('index', { projects });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
module.exports = router;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pm = require('../services/project-manager');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
router.get('/', (req, res) => {
|
|
7
|
+
const projects = pm.getAll();
|
|
8
|
+
res.render('projects', { projects });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
router.get('/add', (req, res) => {
|
|
12
|
+
res.render('project-add');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
router.post('/add', (req, res) => {
|
|
16
|
+
const { name, path } = req.body;
|
|
17
|
+
if (!name || !path || !fs.existsSync(path)) {
|
|
18
|
+
return res.render('project-add', { error: 'Invalid project path or name' });
|
|
19
|
+
}
|
|
20
|
+
const project = pm.add(name, path);
|
|
21
|
+
res.redirect(`/projects/${project.id}`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
router.get('/:id', (req, res) => {
|
|
25
|
+
const project = pm.get(req.params.id);
|
|
26
|
+
if (!project) return res.status(404).render('error', { message: 'Project not found' });
|
|
27
|
+
|
|
28
|
+
const bridge = pm.getBridge(req.params.id);
|
|
29
|
+
const statusResult = bridge.status();
|
|
30
|
+
const list = bridge.listAll();
|
|
31
|
+
|
|
32
|
+
res.render('project', { project, status: statusResult, list });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
router.post('/:id/remove', (req, res) => {
|
|
36
|
+
pm.remove(req.params.id);
|
|
37
|
+
res.redirect('/projects');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
router.post('/:id/run-pipeline', (req, res) => {
|
|
41
|
+
const project = pm.get(req.params.id);
|
|
42
|
+
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
43
|
+
|
|
44
|
+
const bridge = pm.getBridge(req.params.id);
|
|
45
|
+
const { storyName } = req.body;
|
|
46
|
+
|
|
47
|
+
let result;
|
|
48
|
+
if (storyName) {
|
|
49
|
+
result = bridge.runFull(storyName);
|
|
50
|
+
} else {
|
|
51
|
+
result = bridge.status();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
res.json({ success: result.success, output: result.output, error: result.error });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
router.post('/:id/execute', (req, res) => {
|
|
58
|
+
const project = pm.get(req.params.id);
|
|
59
|
+
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
60
|
+
|
|
61
|
+
const bridge = pm.getBridge(req.params.id);
|
|
62
|
+
const { testName } = req.body;
|
|
63
|
+
const result = bridge.execute(testName);
|
|
64
|
+
res.json({ success: result.success, output: result.output, error: result.error, runId: extractRunId(result.output) });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
router.post('/:id/heal', (req, res) => {
|
|
68
|
+
const project = pm.get(req.params.id);
|
|
69
|
+
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
70
|
+
|
|
71
|
+
const bridge = pm.getBridge(req.params.id);
|
|
72
|
+
const { runId } = req.body;
|
|
73
|
+
const result = bridge.heal(runId);
|
|
74
|
+
res.json({ success: result.success, output: result.output, error: result.error });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
router.post('/:id/report', (req, res) => {
|
|
78
|
+
const project = pm.get(req.params.id);
|
|
79
|
+
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
80
|
+
|
|
81
|
+
const bridge = pm.getBridge(req.params.id);
|
|
82
|
+
const { runId } = req.body;
|
|
83
|
+
const result = bridge.report(runId);
|
|
84
|
+
res.json({ success: result.success, output: result.output, error: result.error });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function extractRunId(output) {
|
|
88
|
+
const match = output.match(/run-[\d-T-]+/);
|
|
89
|
+
return match ? match[0] : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = router;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const pm = require('../services/project-manager');
|
|
4
|
+
|
|
5
|
+
router.get('/', (req, res) => {
|
|
6
|
+
const projectId = req.query.project;
|
|
7
|
+
const project = projectId ? pm.get(projectId) : pm.getAll()[0];
|
|
8
|
+
if (!project) return res.redirect('/projects');
|
|
9
|
+
|
|
10
|
+
const bridge = pm.getBridge(project.id);
|
|
11
|
+
const runDirs = bridge.getRunDirs();
|
|
12
|
+
|
|
13
|
+
const hasAllure = bridge.hasAllureReport();
|
|
14
|
+
const runs = runDirs.map(d => {
|
|
15
|
+
const result = bridge.getRunResult(d.name);
|
|
16
|
+
const healing = bridge.getHealingReport(d.name);
|
|
17
|
+
return {
|
|
18
|
+
id: d.name,
|
|
19
|
+
timestamp: d.name.replace('run-', '').replace(/-/g, ':').slice(0, 19),
|
|
20
|
+
success: result ? result.success : null,
|
|
21
|
+
duration: result ? result.duration : null,
|
|
22
|
+
failedCount: result && result.failedTests ? result.failedTests.length : 0,
|
|
23
|
+
passedCount: result && result.passedTests ? result.passedTests.length : 0,
|
|
24
|
+
healedCount: healing ? healing.totalHealed || 0 : 0,
|
|
25
|
+
hasReport: !!bridge.getFinalReport(d.name),
|
|
26
|
+
hasAllure,
|
|
27
|
+
testName: result ? result.test : 'Unknown',
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
res.render('runs', { project, runs });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
router.get('/:runId', (req, res) => {
|
|
35
|
+
const projectId = req.query.project;
|
|
36
|
+
const project = projectId ? pm.get(projectId) : pm.getAll()[0];
|
|
37
|
+
if (!project) return res.redirect('/projects');
|
|
38
|
+
|
|
39
|
+
const bridge = pm.getBridge(project.id);
|
|
40
|
+
const result = bridge.getRunResult(req.params.runId);
|
|
41
|
+
if (!result) return res.status(404).render('error', { message: 'Run not found' });
|
|
42
|
+
|
|
43
|
+
const healing = bridge.getHealingReport(req.params.runId);
|
|
44
|
+
const report = bridge.getFinalReport(req.params.runId);
|
|
45
|
+
const hasAllure = bridge.hasAllureReport();
|
|
46
|
+
|
|
47
|
+
res.render('run', { project, run: { id: req.params.runId, result, healing, report, hasAllure } });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
router.get('/:runId/compare/:otherRunId', (req, res) => {
|
|
51
|
+
const projectId = req.query.project;
|
|
52
|
+
const project = projectId ? pm.get(projectId) : pm.getAll()[0];
|
|
53
|
+
if (!project) return res.redirect('/projects');
|
|
54
|
+
|
|
55
|
+
const bridge = pm.getBridge(project.id);
|
|
56
|
+
const result1 = bridge.getRunResult(req.params.runId);
|
|
57
|
+
const result2 = bridge.getRunResult(req.params.otherRunId);
|
|
58
|
+
if (!result1 || !result2) return res.status(404).render('error', { message: 'Run not found' });
|
|
59
|
+
|
|
60
|
+
res.render('runs', { project, runs: [
|
|
61
|
+
{ id: req.params.runId, success: result1.success, duration: result1.duration, testName: result1.test, failedCount: result1.failedTests ? result1.failedTests.length : 0, healedCount: 0, hasReport: !!bridge.getFinalReport(req.params.runId), timestamp: '' },
|
|
62
|
+
{ id: req.params.otherRunId, success: result2.success, duration: result2.duration, testName: result2.test, failedCount: result2.failedTests ? result2.failedTests.length : 0, healedCount: 0, hasReport: !!bridge.getFinalReport(req.params.otherRunId), timestamp: '' },
|
|
63
|
+
] });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
module.exports = router;
|