@fromeroc9/testform 1.0.3 → 1.0.4
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/action/index.js +1 -1
- package/dist/action.js +60 -0
- package/dist/adapters/github.js +467 -0
- package/dist/adapters/resources.js +363 -0
- package/dist/cli/index.js +3 -3
- package/dist/commands/apply.js +390 -0
- package/dist/commands/destroy.js +85 -0
- package/dist/commands/diff.js +131 -0
- package/dist/commands/fmt.js +166 -0
- package/dist/commands/force-unlock.js +55 -0
- package/dist/commands/generate.js +143 -0
- package/dist/commands/graph.js +159 -0
- package/dist/commands/import.js +222 -0
- package/dist/commands/init.js +167 -0
- package/dist/commands/login.js +71 -0
- package/dist/commands/logout.js +20 -0
- package/dist/commands/plan.js +250 -0
- package/dist/commands/refresh.js +165 -0
- package/dist/commands/report.js +724 -0
- package/dist/commands/show.js +61 -0
- package/dist/commands/state.js +197 -0
- package/dist/commands/taint.js +49 -0
- package/dist/commands/validate.js +128 -0
- package/dist/commands/workspace.js +102 -0
- package/dist/const.js +105 -0
- package/dist/core/backends/azurerm.js +201 -0
- package/dist/core/backends/backend.js +2 -0
- package/dist/core/backends/gcs.js +200 -0
- package/dist/core/backends/local.js +162 -0
- package/dist/core/backends/s3.js +224 -0
- package/dist/core/command-context.js +59 -0
- package/dist/core/config.js +131 -0
- package/dist/core/credentials.js +53 -0
- package/dist/core/parser.js +62 -0
- package/dist/core/parsers/base-parser.js +215 -0
- package/dist/core/parsers/testcase-parser.js +115 -0
- package/dist/core/parsers/testplan-parser.js +41 -0
- package/dist/core/parsers/testrun-parser.js +43 -0
- package/dist/core/policy.js +341 -0
- package/dist/core/prompt.js +109 -0
- package/dist/core/state.js +185 -0
- package/dist/core/utils.js +94 -0
- package/dist/core/variables.js +108 -0
- package/dist/core/workspace.js +56 -0
- package/dist/help.js +797 -0
- package/dist/index.js +650 -0
- package/dist/logger.js +134 -0
- package/dist/notify.js +36 -0
- package/dist/types.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.reportCmd = void 0;
|
|
7
|
+
exports.resolvePath = resolvePath;
|
|
8
|
+
const state_1 = require("../core/state");
|
|
9
|
+
const chalk_1 = require("chalk");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const config_1 = require("../core/config");
|
|
12
|
+
const github_1 = require("../adapters/github");
|
|
13
|
+
function resolvePath(obj, path) {
|
|
14
|
+
if (!path)
|
|
15
|
+
return undefined;
|
|
16
|
+
const parts = path.replace(/\[(\w+)\]/g, '.$1').split('.');
|
|
17
|
+
let current = obj;
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (current === undefined || current === null)
|
|
20
|
+
return undefined;
|
|
21
|
+
current = current[part];
|
|
22
|
+
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
const reportCmd = async (options) => {
|
|
26
|
+
const { dir, type, format, filter, out, groupBy, statePath, apply, fields } = options;
|
|
27
|
+
const stateObj = new state_1.State(dir, statePath);
|
|
28
|
+
await stateObj.init();
|
|
29
|
+
// 1. Extract data into a unified FlatData array
|
|
30
|
+
const testcases = stateObj.getResources('github_testcase');
|
|
31
|
+
const testruns = stateObj.getResources('github_testrun');
|
|
32
|
+
const testplans = stateObj.getResources('github_testplan');
|
|
33
|
+
// Build map for quick access
|
|
34
|
+
const runMap = new Map();
|
|
35
|
+
for (const r of testruns)
|
|
36
|
+
runMap.set(r.identity, r);
|
|
37
|
+
// Flat data array
|
|
38
|
+
let data = [];
|
|
39
|
+
// Since testcases might be executed in multiple testruns, we iterate testruns -> testcases
|
|
40
|
+
// But we also want unexecuted testcases.
|
|
41
|
+
const executedTestcases = new Set();
|
|
42
|
+
for (const run of testruns) {
|
|
43
|
+
const statuses = run.attributes.testcaseStatuses || {};
|
|
44
|
+
for (const [tcIdentity, status] of Object.entries(statuses)) {
|
|
45
|
+
const tc = testcases.find(t => t.identity === tcIdentity);
|
|
46
|
+
if (!tc)
|
|
47
|
+
continue;
|
|
48
|
+
executedTestcases.add(tcIdentity);
|
|
49
|
+
data.push({
|
|
50
|
+
id: tcIdentity,
|
|
51
|
+
type: 'github_testcase',
|
|
52
|
+
title: String(tc.attributes.title || ''),
|
|
53
|
+
status: status || 'pending',
|
|
54
|
+
labels: Array.isArray(tc.attributes.labels) ? tc.attributes.labels.map(String) : [],
|
|
55
|
+
assignees: Array.isArray(tc.attributes.assignees) ? tc.attributes.assignees.map(String) : [],
|
|
56
|
+
milestone: String(tc.attributes.milestone || ''),
|
|
57
|
+
testRunId: run.identity,
|
|
58
|
+
testPlanId: '', // To be mapped if needed
|
|
59
|
+
issueNumber: Number(tc.attributes.issueNumber || 0),
|
|
60
|
+
issueUrl: `https://github.com/issues/${tc.attributes.issueNumber}`,
|
|
61
|
+
custom_fields: tc.attributes.custom_fields || {},
|
|
62
|
+
createdAt: String(tc.attributes.createdAt || tc.lastApplied || new Date().toISOString()),
|
|
63
|
+
updatedAt: String(tc.attributes.updatedAt || tc.lastApplied || new Date().toISOString()),
|
|
64
|
+
originalResource: tc
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Add testcases that are not in any run
|
|
69
|
+
for (const tc of testcases) {
|
|
70
|
+
if (!executedTestcases.has(tc.identity)) {
|
|
71
|
+
data.push({
|
|
72
|
+
id: tc.identity,
|
|
73
|
+
type: 'github_testcase',
|
|
74
|
+
title: String(tc.attributes.title || ''),
|
|
75
|
+
status: 'unexecuted',
|
|
76
|
+
labels: Array.isArray(tc.attributes.labels) ? tc.attributes.labels.map(String) : [],
|
|
77
|
+
assignees: Array.isArray(tc.attributes.assignees) ? tc.attributes.assignees.map(String) : [],
|
|
78
|
+
milestone: String(tc.attributes.milestone || ''),
|
|
79
|
+
testRunId: '',
|
|
80
|
+
testPlanId: '',
|
|
81
|
+
issueNumber: Number(tc.attributes.issueNumber || 0),
|
|
82
|
+
issueUrl: `https://github.com/issues/${tc.attributes.issueNumber}`,
|
|
83
|
+
custom_fields: tc.attributes.custom_fields || {},
|
|
84
|
+
createdAt: String(tc.attributes.createdAt || tc.lastApplied || new Date().toISOString()),
|
|
85
|
+
updatedAt: String(tc.attributes.updatedAt || tc.lastApplied || new Date().toISOString()),
|
|
86
|
+
originalResource: tc
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// 2. Apply Filters
|
|
91
|
+
for (const f of filter) {
|
|
92
|
+
const [key, val] = f.split('=');
|
|
93
|
+
if (!key || !val)
|
|
94
|
+
continue;
|
|
95
|
+
data = data.filter(item => {
|
|
96
|
+
const itemVal = item[key] || item.custom_fields[key];
|
|
97
|
+
if (Array.isArray(itemVal)) {
|
|
98
|
+
return itemVal.includes(val);
|
|
99
|
+
}
|
|
100
|
+
return String(itemVal) === val;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// 3. Generate Report
|
|
104
|
+
let content = '';
|
|
105
|
+
if (format === 'json') {
|
|
106
|
+
content = JSON.stringify(data, null, 2);
|
|
107
|
+
}
|
|
108
|
+
else if (format === 'csv') {
|
|
109
|
+
const headers = ['ID', 'Title', 'Status', 'Labels', 'Assignees', 'Milestone', 'TestRun', 'IssueNumber'];
|
|
110
|
+
const rows = data.map(d => [
|
|
111
|
+
d.id,
|
|
112
|
+
`"${d.title.replace(/"/g, '""')}"`,
|
|
113
|
+
d.status,
|
|
114
|
+
`"${d.labels.join(' ')}"`,
|
|
115
|
+
`"${d.assignees.join(' ')}"`,
|
|
116
|
+
`"${d.milestone}"`,
|
|
117
|
+
d.testRunId,
|
|
118
|
+
d.issueNumber
|
|
119
|
+
]);
|
|
120
|
+
content = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Markdown format
|
|
124
|
+
const config = new config_1.Config(dir);
|
|
125
|
+
content = generateMarkdownReport(type, data, groupBy, config, testruns, testplans);
|
|
126
|
+
}
|
|
127
|
+
// Output
|
|
128
|
+
if (out) {
|
|
129
|
+
const path = require('path');
|
|
130
|
+
const resolvedOut = path.resolve(dir, out);
|
|
131
|
+
fs_1.default.writeFileSync(resolvedOut, content, 'utf-8');
|
|
132
|
+
console.log((0, chalk_1.green)(`Report saved to ${out}`));
|
|
133
|
+
}
|
|
134
|
+
else if (!apply) {
|
|
135
|
+
console.log(content);
|
|
136
|
+
}
|
|
137
|
+
// Apply to GitHub
|
|
138
|
+
if (apply) {
|
|
139
|
+
if (format !== 'md') {
|
|
140
|
+
console.log((0, chalk_1.yellow)('Warning: Uploading to GitHub is recommended in "md" format. Currently using ' + format));
|
|
141
|
+
}
|
|
142
|
+
const config = new config_1.Config(dir);
|
|
143
|
+
const ghConfig = config.getGitHub();
|
|
144
|
+
if (!ghConfig) {
|
|
145
|
+
console.error((0, chalk_1.red)('Error: GitHub configuration not found in testform.json'));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const reportFields = config.getFields('testreport');
|
|
149
|
+
const customFieldsMap = {};
|
|
150
|
+
let assignees = [];
|
|
151
|
+
let milestone = undefined;
|
|
152
|
+
let issueLabels = ['testreport'];
|
|
153
|
+
// Parse --field arguments
|
|
154
|
+
for (const f of fields || []) {
|
|
155
|
+
let parsedField = {};
|
|
156
|
+
if (f.trim().startsWith('{')) {
|
|
157
|
+
try {
|
|
158
|
+
parsedField = JSON.parse(f);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
console.log((0, chalk_1.yellow)(`Warning: Could not parse field JSON: ${f}`));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const parts = f.split('=');
|
|
167
|
+
if (parts.length >= 2) {
|
|
168
|
+
parsedField[parts[0]] = parts.slice(1).join('=');
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log((0, chalk_1.yellow)(`Warning: Invalid field format: ${f}. Expected key=value or JSON`));
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const [k, v] of Object.entries(parsedField)) {
|
|
176
|
+
const def = reportFields.find(df => df.name.toLowerCase() === k.toLowerCase());
|
|
177
|
+
if (k.toLowerCase() === 'assignees') {
|
|
178
|
+
assignees = assignees.concat(v.split(',').map(s => s.trim().replace(/^@/, '')));
|
|
179
|
+
}
|
|
180
|
+
else if (k.toLowerCase() === 'milestone') {
|
|
181
|
+
milestone = v;
|
|
182
|
+
}
|
|
183
|
+
else if (def?.type === 'tags') {
|
|
184
|
+
issueLabels.push(v);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
customFieldsMap[k] = v;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const github = new github_1.GitHubAdapter(ghConfig);
|
|
192
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
193
|
+
const titleType = type.charAt(0).toUpperCase() + type.slice(1).replace('-', ' ');
|
|
194
|
+
const issueTitle = `${titleType} - ${dateStr}`;
|
|
195
|
+
console.log(`Uploading report to GitHub...`);
|
|
196
|
+
const result = await github.createIssue({
|
|
197
|
+
title: issueTitle,
|
|
198
|
+
body: content,
|
|
199
|
+
labels: [...new Set(issueLabels)],
|
|
200
|
+
assignees: assignees.length > 0 ? assignees : undefined,
|
|
201
|
+
milestone: milestone ? parseInt(milestone, 10) : undefined
|
|
202
|
+
});
|
|
203
|
+
console.log((0, chalk_1.green)(`✅ Report successfully created: https://github.com/${ghConfig.owner}/${ghConfig.repository}/issues/${result.number}`));
|
|
204
|
+
if (result.node_id && ghConfig.projectId) {
|
|
205
|
+
try {
|
|
206
|
+
const itemId = await github.addToProject(result.node_id);
|
|
207
|
+
if (itemId) {
|
|
208
|
+
console.log((0, chalk_1.green)(`✅ Issue added to GitHub Project.`));
|
|
209
|
+
if (Object.keys(customFieldsMap).length > 0) {
|
|
210
|
+
console.log(`Setting custom fields...`);
|
|
211
|
+
await github.updateProjectItemFields(itemId, customFieldsMap);
|
|
212
|
+
console.log((0, chalk_1.green)(`✅ Custom fields updated successfully.`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
console.log((0, chalk_1.yellow)(`Warning: Could not add to project or update custom fields. ${e.message}`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
exports.reportCmd = reportCmd;
|
|
223
|
+
const ICONS = {
|
|
224
|
+
passed: '✅',
|
|
225
|
+
failed: '❌',
|
|
226
|
+
pending: '⏳',
|
|
227
|
+
blocked: '⚠️',
|
|
228
|
+
skipped: '⏭️',
|
|
229
|
+
unexecuted: '❔'
|
|
230
|
+
};
|
|
231
|
+
function generateMarkdownReport(type, data, groupBy = 'labels', config, testruns = [], testplans = []) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
if (type === 'testcase-summary') {
|
|
234
|
+
lines.push('# Informe de Casos de Prueba');
|
|
235
|
+
lines.push('');
|
|
236
|
+
lines.push('| ID | Título | Etiquetas | Estado |');
|
|
237
|
+
lines.push('|---|---|---|---|');
|
|
238
|
+
data.forEach(d => {
|
|
239
|
+
const icon = ICONS[d.status.toLowerCase()] || ICONS.unexecuted;
|
|
240
|
+
lines.push(`| ${d.id} | ${d.title} | ${d.labels.join(', ')} | ${icon} ${d.status} |`);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else if (type === 'testrun-summary') {
|
|
244
|
+
lines.push('# Test Run Summary');
|
|
245
|
+
lines.push('');
|
|
246
|
+
const totalRuns = testruns.length;
|
|
247
|
+
const activeRuns = testruns.filter(r => r.attributes.state === 'open').length;
|
|
248
|
+
const closedRuns = testruns.filter(r => r.attributes.state === 'closed').length;
|
|
249
|
+
const totalTestCases = new Set(data.filter(d => d.testRunId).map(d => d.id)).size;
|
|
250
|
+
const requirementsCount = new Set(data.filter(d => d.testRunId).map(d => d.id.split('::')[0])).size;
|
|
251
|
+
const failuresCount = data.filter(d => d.testRunId && ['failed', 'blocked'].includes(d.status.toLowerCase())).length;
|
|
252
|
+
// 1. Chart: Total Test Runs (Half Doughnut)
|
|
253
|
+
const runsChartObj = {
|
|
254
|
+
type: 'doughnut',
|
|
255
|
+
data: {
|
|
256
|
+
labels: ['Active', 'Closed'],
|
|
257
|
+
datasets: [{ data: [activeRuns, closedRuns], backgroundColor: ['#f39c12', '#3498db'] }]
|
|
258
|
+
},
|
|
259
|
+
options: {
|
|
260
|
+
rotation: 270,
|
|
261
|
+
circumference: 180,
|
|
262
|
+
plugins: { doughnutlabel: { labels: [{ text: totalRuns.toString(), font: { size: 40 } }, { text: 'Test Runs' }] }, legend: { position: 'right' } }
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const runsChartUrl = `https://quickchart.io/chart?w=400&h=200&c=${encodeURIComponent(JSON.stringify(runsChartObj))}`;
|
|
266
|
+
// 2. Chart: Test Case Break-up
|
|
267
|
+
const stats = getStats(data.filter(d => d.testRunId));
|
|
268
|
+
const tcBreakupObj = {
|
|
269
|
+
type: 'doughnut',
|
|
270
|
+
data: {
|
|
271
|
+
labels: ['Passed', 'Failed', 'Blocked', 'Pending', 'Skipped'],
|
|
272
|
+
datasets: [{ data: [stats.passed, stats.failed, stats.blocked, stats.pending, stats.skipped], backgroundColor: ['#2ea043', '#f85149', '#58a6ff', '#e3b341', '#8b949e'] }]
|
|
273
|
+
},
|
|
274
|
+
options: {
|
|
275
|
+
plugins: { doughnutlabel: { labels: [{ text: totalTestCases.toString(), font: { size: 30 } }, { text: 'Total Test Cases' }] }, legend: { position: 'right' } }
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const tcBreakupUrl = `https://quickchart.io/chart?w=400&h=200&c=${encodeURIComponent(JSON.stringify(tcBreakupObj))}`;
|
|
279
|
+
// 3. Chart: Test Runs Break-up (Bar chart over time)
|
|
280
|
+
const dateCounts = {};
|
|
281
|
+
for (const run of testruns) {
|
|
282
|
+
const date = (run.createdAt || run.lastApplied || new Date().toISOString()).split('T')[0];
|
|
283
|
+
dateCounts[date] = (dateCounts[date] || 0) + 1;
|
|
284
|
+
}
|
|
285
|
+
const dates = Object.keys(dateCounts).sort();
|
|
286
|
+
const counts = dates.map(d => dateCounts[d]);
|
|
287
|
+
const barChartObj = {
|
|
288
|
+
type: 'bar',
|
|
289
|
+
data: { labels: dates.length ? dates : ['No Data'], datasets: [{ label: 'Test Runs', data: counts.length ? counts : [0], backgroundColor: '#3498db' }] },
|
|
290
|
+
options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
|
291
|
+
};
|
|
292
|
+
const barChartUrl = `https://quickchart.io/chart?w=400&h=200&c=${encodeURIComponent(JSON.stringify(barChartObj))}`;
|
|
293
|
+
// HTML Layout
|
|
294
|
+
lines.push('<table width="100%">');
|
|
295
|
+
lines.push('<tr><td width="50%">');
|
|
296
|
+
lines.push('<h3>Total Test Runs</h3>');
|
|
297
|
+
lines.push(`<img src="${runsChartUrl}" />`);
|
|
298
|
+
lines.push('</td><td width="50%" valign="top">');
|
|
299
|
+
lines.push('<h3>Total Test Cases</h3>');
|
|
300
|
+
lines.push(`<h1>${totalTestCases}</h1><br>`);
|
|
301
|
+
lines.push('<h3>Total Linked Requirements</h3>');
|
|
302
|
+
lines.push(`<h1>${requirementsCount}</h1>`);
|
|
303
|
+
lines.push('</td></tr>');
|
|
304
|
+
lines.push('</table>');
|
|
305
|
+
lines.push('<table width="100%">');
|
|
306
|
+
lines.push('<tr><td width="50%">');
|
|
307
|
+
lines.push('<h3>Test Case Break-up</h3>');
|
|
308
|
+
lines.push(`<img src="${tcBreakupUrl}" />`);
|
|
309
|
+
lines.push('</td><td width="50%">');
|
|
310
|
+
lines.push('<h3>Test Runs Break-up</h3>');
|
|
311
|
+
lines.push(`<img src="${barChartUrl}" />`);
|
|
312
|
+
lines.push('</td></tr>');
|
|
313
|
+
lines.push('</table>');
|
|
314
|
+
lines.push('<table width="100%">');
|
|
315
|
+
lines.push('<tr><td width="50%">');
|
|
316
|
+
lines.push('<h3>Defects Linked with Test Results</h3>');
|
|
317
|
+
lines.push(`<h1>${failuresCount}</h1>`);
|
|
318
|
+
lines.push('</td><td width="50%">');
|
|
319
|
+
lines.push('<h3>Requirements Linked with Test Runs</h3>');
|
|
320
|
+
lines.push(`<h1>${requirementsCount}</h1>`);
|
|
321
|
+
lines.push('</td></tr>');
|
|
322
|
+
lines.push('</table>');
|
|
323
|
+
}
|
|
324
|
+
else if (type === 'testrun-detailed') {
|
|
325
|
+
lines.push('# Test Run Detailed Report');
|
|
326
|
+
lines.push('');
|
|
327
|
+
const totalRuns = testruns.length;
|
|
328
|
+
const activeRuns = testruns.filter(r => r.attributes.state === 'open').length;
|
|
329
|
+
const closedRuns = testruns.filter(r => r.attributes.state === 'closed').length;
|
|
330
|
+
const totalTestCases = new Set(data.filter(d => d.testRunId).map(d => d.id)).size;
|
|
331
|
+
const requirementsCount = new Set(data.filter(d => d.testRunId).map(d => d.id.split('::')[0])).size;
|
|
332
|
+
// Chart: Trend line over specific test runs
|
|
333
|
+
const runCounts = {};
|
|
334
|
+
for (const d of data.filter(d => d.testRunId)) {
|
|
335
|
+
runCounts[d.testRunId] = (runCounts[d.testRunId] || 0) + 1;
|
|
336
|
+
}
|
|
337
|
+
const runLabels = Object.keys(runCounts).sort();
|
|
338
|
+
const runData = runLabels.map(r => runCounts[r]);
|
|
339
|
+
const trendChartObj = {
|
|
340
|
+
type: 'line',
|
|
341
|
+
data: { labels: runLabels.length ? runLabels : ['No Data'], datasets: [{ label: 'Test Cases', data: runData.length ? runData : [0], borderColor: '#3498db', fill: false }] },
|
|
342
|
+
options: { plugins: { legend: { display: false } } }
|
|
343
|
+
};
|
|
344
|
+
const trendChartUrl = `https://quickchart.io/chart?w=500&h=200&c=${encodeURIComponent(JSON.stringify(trendChartObj))}`;
|
|
345
|
+
lines.push('<table width="100%">');
|
|
346
|
+
lines.push('<tr><td width="60%">');
|
|
347
|
+
lines.push('<h3>Test run performance</h3>');
|
|
348
|
+
lines.push(`<h1>${totalTestCases}</h1> <small>Test Cases trend over Specific Test Runs</small><br/>`);
|
|
349
|
+
lines.push(`<img src="${trendChartUrl}" />`);
|
|
350
|
+
lines.push('</td><td width="40%" valign="top">');
|
|
351
|
+
lines.push('<table width="100%"><tr><td>');
|
|
352
|
+
lines.push('<h3>Active Test Runs</h3>');
|
|
353
|
+
lines.push(`<h2>${activeRuns} / ${totalRuns}</h2>`);
|
|
354
|
+
lines.push('</td><td>');
|
|
355
|
+
lines.push('<h3>Closed Test Runs</h3>');
|
|
356
|
+
lines.push(`<h2>${closedRuns} / ${totalRuns}</h2>`);
|
|
357
|
+
lines.push('</td></tr><tr><td>');
|
|
358
|
+
lines.push('<h3>Total Test Cases</h3>');
|
|
359
|
+
lines.push(`<h2>${totalTestCases}</h2>`);
|
|
360
|
+
lines.push('</td><td>');
|
|
361
|
+
lines.push('<h3>Total Linked Issues</h3>');
|
|
362
|
+
lines.push(`<h2>${requirementsCount}</h2>`);
|
|
363
|
+
lines.push('</td></tr></table>');
|
|
364
|
+
lines.push('</td></tr>');
|
|
365
|
+
lines.push('</table>');
|
|
366
|
+
lines.push('');
|
|
367
|
+
lines.push(`### ${totalTestCases} Test cases included in this report`);
|
|
368
|
+
lines.push('');
|
|
369
|
+
lines.push('| TEST RUN | TEST CASE | TEST RUN LATEST STATUS | TEST CASE PRIORITY |');
|
|
370
|
+
lines.push('|---|---|---|---|');
|
|
371
|
+
const priorityPath = config.getReportMapping('priority') || 'attributes.custom_fields.priority';
|
|
372
|
+
for (const d of data.filter(d => d.testRunId)) {
|
|
373
|
+
let priority = resolvePath(d.originalResource, priorityPath);
|
|
374
|
+
if (priority === undefined)
|
|
375
|
+
priority = 'Medium';
|
|
376
|
+
const icon = ICONS[d.status.toLowerCase()] || ICONS.unexecuted;
|
|
377
|
+
lines.push(`| ${d.testRunId} | **${d.id}**<br/>${d.title} | ${icon} ${d.status} | ${priority} |`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else if (type === 'testplan-summary') {
|
|
381
|
+
lines.push('# Test Plan Summary');
|
|
382
|
+
lines.push('');
|
|
383
|
+
const linkedTestRunIds = new Set();
|
|
384
|
+
for (const plan of testplans) {
|
|
385
|
+
const planRuns = plan.attributes.testruns || [];
|
|
386
|
+
for (const r of planRuns) {
|
|
387
|
+
const matchingRun = testruns.find(tr => tr.identity.endsWith(r));
|
|
388
|
+
if (matchingRun)
|
|
389
|
+
linkedTestRunIds.add(matchingRun.identity);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const planData = data.filter(d => d.testRunId && linkedTestRunIds.has(d.testRunId));
|
|
393
|
+
const totalPlanTestCases = planData.length;
|
|
394
|
+
const stats = getStats(planData);
|
|
395
|
+
const planProgressObj = {
|
|
396
|
+
type: 'doughnut',
|
|
397
|
+
data: {
|
|
398
|
+
labels: ['Passed', 'Failed', 'Blocked', 'Pending', 'Skipped'],
|
|
399
|
+
datasets: [{ data: [stats.passed, stats.failed, stats.blocked, stats.pending, stats.skipped], backgroundColor: ['#2ea043', '#f85149', '#58a6ff', '#e3b341', '#8b949e'] }]
|
|
400
|
+
},
|
|
401
|
+
options: {
|
|
402
|
+
plugins: { doughnutlabel: { labels: [{ text: totalPlanTestCases.toString(), font: { size: 30 } }, { text: 'Total Test Cases' }] }, legend: { position: 'right' } }
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const planProgressUrl = `https://quickchart.io/chart?w=400&h=200&c=${encodeURIComponent(JSON.stringify(planProgressObj))}`;
|
|
406
|
+
const runResults = {};
|
|
407
|
+
for (const d of planData) {
|
|
408
|
+
if (!runResults[d.testRunId])
|
|
409
|
+
runResults[d.testRunId] = { passed: 0, failed: 0 };
|
|
410
|
+
if (d.status.toLowerCase() === 'passed')
|
|
411
|
+
runResults[d.testRunId].passed++;
|
|
412
|
+
if (d.status.toLowerCase() === 'failed')
|
|
413
|
+
runResults[d.testRunId].failed++;
|
|
414
|
+
}
|
|
415
|
+
const runLabels = Object.keys(runResults).sort();
|
|
416
|
+
const passedData = runLabels.map(r => runResults[r].passed);
|
|
417
|
+
const failedData = runLabels.map(r => runResults[r].failed);
|
|
418
|
+
const planBarObj = {
|
|
419
|
+
type: 'bar',
|
|
420
|
+
data: {
|
|
421
|
+
labels: runLabels.length ? runLabels : ['No Data'],
|
|
422
|
+
datasets: [
|
|
423
|
+
{ label: 'Passed', data: passedData.length ? passedData : [0], backgroundColor: '#2ea043' },
|
|
424
|
+
{ label: 'Failed', data: failedData.length ? failedData : [0], backgroundColor: '#f85149' }
|
|
425
|
+
]
|
|
426
|
+
},
|
|
427
|
+
options: { scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true } } }
|
|
428
|
+
};
|
|
429
|
+
const planBarUrl = `https://quickchart.io/chart?w=400&h=200&c=${encodeURIComponent(JSON.stringify(planBarObj))}`;
|
|
430
|
+
lines.push('<table width="100%">');
|
|
431
|
+
lines.push('<tr><td width="50%">');
|
|
432
|
+
lines.push('<h3>Overall Test Plan Progress</h3>');
|
|
433
|
+
lines.push(`<img src="${planProgressUrl}" />`);
|
|
434
|
+
lines.push('</td><td width="50%">');
|
|
435
|
+
lines.push('<h3>Results from All Linked Test Runs</h3>');
|
|
436
|
+
lines.push(`<img src="${planBarUrl}" />`);
|
|
437
|
+
lines.push('</td></tr>');
|
|
438
|
+
lines.push('</table>');
|
|
439
|
+
lines.push('');
|
|
440
|
+
lines.push(`### ${linkedTestRunIds.size} test runs linked to these test plans`);
|
|
441
|
+
lines.push('');
|
|
442
|
+
lines.push('| RUNS | TESTS | TESTS STATUS |');
|
|
443
|
+
lines.push('|---|---|---|');
|
|
444
|
+
for (const runId of Array.from(linkedTestRunIds)) {
|
|
445
|
+
const runItems = planData.filter(d => d.testRunId === runId);
|
|
446
|
+
const rStats = getStats(runItems);
|
|
447
|
+
lines.push(`| **${runId}** | ${runItems.length} | 🟢 ${rStats.passed} 🔴 ${rStats.failed} ⏳ ${rStats.pending} |`);
|
|
448
|
+
}
|
|
449
|
+
lines.push('');
|
|
450
|
+
lines.push('<details><summary><b>Linked Test Cases</b></summary>');
|
|
451
|
+
lines.push('');
|
|
452
|
+
lines.push('| TEST RUN | ID | TITLE | PRIORITY | TYPE OF TEST | STATUS |');
|
|
453
|
+
lines.push('|---|---|---|---|---|---|');
|
|
454
|
+
const priorityPath = config.getReportMapping('priority') || 'attributes.custom_fields.priority';
|
|
455
|
+
const typePath = config.getReportMapping('type') || 'attributes.custom_fields.type';
|
|
456
|
+
for (const d of planData) {
|
|
457
|
+
let priority = resolvePath(d.originalResource, priorityPath) || '--';
|
|
458
|
+
let testType = resolvePath(d.originalResource, typePath) || '--';
|
|
459
|
+
const icon = ICONS[d.status.toLowerCase()] || ICONS.unexecuted;
|
|
460
|
+
lines.push(`| ${d.testRunId} | ${d.id} | ${d.title} | ${priority} | ${testType} | ${icon} ${d.status} |`);
|
|
461
|
+
}
|
|
462
|
+
lines.push('');
|
|
463
|
+
lines.push('</details>');
|
|
464
|
+
}
|
|
465
|
+
else if (type === 'defects') {
|
|
466
|
+
lines.push('# Informe de Defectos');
|
|
467
|
+
lines.push('');
|
|
468
|
+
const failures = data.filter(d => ['failed', 'blocked'].includes(d.status.toLowerCase()));
|
|
469
|
+
if (failures.length === 0) {
|
|
470
|
+
lines.push('🎉 No hay defectos reportados.');
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
lines.push('| Run | Test Case | Título | Issue |');
|
|
474
|
+
lines.push('|---|---|---|---|');
|
|
475
|
+
failures.forEach(d => {
|
|
476
|
+
lines.push(`| ${d.testRunId} | ${d.id} | ${d.title} | [#${d.issueNumber}](${d.issueUrl}) |`);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (type === 'traceability') {
|
|
481
|
+
lines.push('# Requirement Traceability Report');
|
|
482
|
+
lines.push('');
|
|
483
|
+
let byGroup = {};
|
|
484
|
+
if (groupBy && groupBy !== 'labels') {
|
|
485
|
+
// Si el usuario pasa explícitamente un --groupBy, lo respetamos (ej. milestone, attributes.custom_fields.sprint)
|
|
486
|
+
byGroup = groupByField(data, groupBy);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// Por defecto, agrupamos por el archivo .feature (Requisito) extrayéndolo del ID
|
|
490
|
+
for (const d of data) {
|
|
491
|
+
const featurePath = d.id.split('::')[0] || 'Unknown';
|
|
492
|
+
if (!byGroup[featurePath])
|
|
493
|
+
byGroup[featurePath] = [];
|
|
494
|
+
byGroup[featurePath].push(d);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const totalReqs = Object.keys(byGroup).length;
|
|
498
|
+
lines.push(`[]()`);
|
|
499
|
+
lines.push('');
|
|
500
|
+
for (const [reqName, items] of Object.entries(byGroup)) {
|
|
501
|
+
const stats = getStats(items);
|
|
502
|
+
const total = items.length;
|
|
503
|
+
const pct = total > 0 ? Math.round((stats.passed / total) * 100) : 0;
|
|
504
|
+
let color = 'red';
|
|
505
|
+
if (pct === 100)
|
|
506
|
+
color = 'success';
|
|
507
|
+
else if (pct > 50)
|
|
508
|
+
color = 'yellow';
|
|
509
|
+
lines.push('<details>');
|
|
510
|
+
lines.push(`<summary><b>${reqName}</b> <img src="https://img.shields.io/badge/Coverage-${pct}%25-${color}"></summary>`);
|
|
511
|
+
lines.push('<br>');
|
|
512
|
+
lines.push('<table width="100%">');
|
|
513
|
+
lines.push(' <tr><th>Test Case</th><th>Status</th><th>Assignee</th></tr>');
|
|
514
|
+
items.forEach(d => {
|
|
515
|
+
const icon = ICONS[d.status.toLowerCase()] || ICONS.unexecuted;
|
|
516
|
+
const assignee = d.assignees[0] ? `@${d.assignees[0]}` : 'Unassigned';
|
|
517
|
+
lines.push(` <tr>`);
|
|
518
|
+
lines.push(` <td><code>${d.id}</code><br>${d.title}</td>`);
|
|
519
|
+
lines.push(` <td align="center">${icon} ${d.status}</td>`);
|
|
520
|
+
lines.push(` <td align="center">${assignee}</td>`);
|
|
521
|
+
lines.push(` </tr>`);
|
|
522
|
+
});
|
|
523
|
+
lines.push('</table>');
|
|
524
|
+
lines.push('</details>');
|
|
525
|
+
lines.push('');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else if (type === 'coverage') {
|
|
529
|
+
lines.push('# Informe de Cobertura');
|
|
530
|
+
lines.push('');
|
|
531
|
+
const byTag = groupByTags(data);
|
|
532
|
+
lines.push('| Etiqueta | Total | ✅ Passed | ❌ Failed | Cobertura % |');
|
|
533
|
+
lines.push('|---|---|---|---|---|');
|
|
534
|
+
for (const [tag, items] of Object.entries(byTag)) {
|
|
535
|
+
const stats = getStats(items);
|
|
536
|
+
const total = items.length;
|
|
537
|
+
const pct = total > 0 ? Math.round((stats.passed / total) * 100) : 0;
|
|
538
|
+
lines.push(`| ${tag} | ${total} | ${stats.passed} | ${stats.failed} | ${pct}% |`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else if (type === 'two-dimensional') {
|
|
542
|
+
lines.push('# Informe Bidimensional');
|
|
543
|
+
lines.push('');
|
|
544
|
+
const byTag = groupByTags(data);
|
|
545
|
+
const allStatuses = ['passed', 'failed', 'pending', 'blocked', 'skipped', 'unexecuted'];
|
|
546
|
+
// Header
|
|
547
|
+
const header = ['Etiqueta', ...allStatuses.map(s => `${ICONS[s]} ${s}`)];
|
|
548
|
+
lines.push(`| ${header.join(' | ')} |`);
|
|
549
|
+
lines.push(`|${header.map(() => '---').join('|')}|`);
|
|
550
|
+
for (const [tag, items] of Object.entries(byTag)) {
|
|
551
|
+
const stats = getStats(items);
|
|
552
|
+
const row = [tag];
|
|
553
|
+
for (const s of allStatuses) {
|
|
554
|
+
row.push(String(stats[s] || 0));
|
|
555
|
+
}
|
|
556
|
+
lines.push(`| ${row.join(' | ')} |`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else if (type === 'test-case-activity') {
|
|
560
|
+
lines.push('# Test Case Activity');
|
|
561
|
+
lines.push('');
|
|
562
|
+
// 1. Calculate Summary
|
|
563
|
+
const total = data.length;
|
|
564
|
+
const updated = data.filter(d => d.updatedAt && d.createdAt && d.updatedAt !== d.createdAt).length;
|
|
565
|
+
const deleted = 0; // Not tracked in state currently
|
|
566
|
+
lines.push(`[]() []() []()`);
|
|
567
|
+
lines.push('');
|
|
568
|
+
const autoField = config.getReportMapping('automation') || 'attributes.custom_fields.automate';
|
|
569
|
+
// 2. Automation Coverage (Doughnut Chart via QuickChart)
|
|
570
|
+
let autoNotReq = 0, automated = 0, notAutomated = 0, cannotBeAutomated = 0;
|
|
571
|
+
for (const d of data) {
|
|
572
|
+
const valFromPath = resolvePath(d.originalResource, autoField) || resolvePath(d.originalResource, 'attributes.custom_fields.automation');
|
|
573
|
+
const automateValue = String(valFromPath || '').toLowerCase();
|
|
574
|
+
if (automateValue === 'true' || automateValue === 'yes' || automateValue === 'automated') {
|
|
575
|
+
automated++;
|
|
576
|
+
}
|
|
577
|
+
else if (automateValue === 'not required') {
|
|
578
|
+
autoNotReq++;
|
|
579
|
+
}
|
|
580
|
+
else if (automateValue === 'cannot be automated') {
|
|
581
|
+
cannotBeAutomated++;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
notAutomated++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const autoChartData = {
|
|
588
|
+
type: 'doughnut',
|
|
589
|
+
data: {
|
|
590
|
+
labels: ['Not Required', 'Automated', 'Not Automated', 'Cannot Be'],
|
|
591
|
+
datasets: [{ data: [autoNotReq, automated, notAutomated, cannotBeAutomated], backgroundColor: ['#2ea043', '#1f6feb', '#d29922', '#f85149'], borderWidth: 0 }]
|
|
592
|
+
},
|
|
593
|
+
options: { plugins: { datalabels: { display: false }, legend: { labels: { fontSize: 14 } } } }
|
|
594
|
+
};
|
|
595
|
+
const autoChartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify(autoChartData))}&w=600&h=300`;
|
|
596
|
+
const trendMap = {};
|
|
597
|
+
for (const d of data) {
|
|
598
|
+
const dateStr = d.createdAt ? d.createdAt.split('T')[0] : new Date().toISOString().split('T')[0];
|
|
599
|
+
trendMap[dateStr] = (trendMap[dateStr] || 0) + 1;
|
|
600
|
+
}
|
|
601
|
+
const sortedDates = Object.keys(trendMap).sort();
|
|
602
|
+
const trendCounts = sortedDates.map(date => trendMap[date]);
|
|
603
|
+
let trendChartUrl = '';
|
|
604
|
+
if (sortedDates.length > 0) {
|
|
605
|
+
const trendChartData = {
|
|
606
|
+
type: 'bar',
|
|
607
|
+
data: {
|
|
608
|
+
labels: sortedDates,
|
|
609
|
+
datasets: [{ label: 'Created', data: trendCounts, backgroundColor: '#8957e5', borderRadius: 4 }]
|
|
610
|
+
},
|
|
611
|
+
options: {
|
|
612
|
+
plugins: { legend: { display: false } },
|
|
613
|
+
scales: {
|
|
614
|
+
xAxes: [{ gridLines: { display: false } }],
|
|
615
|
+
yAxes: [{ ticks: { stepSize: 1, beginAtZero: true }, gridLines: { color: '#e1e4e8' } }]
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
trendChartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify(trendChartData))}&w=600&h=300`;
|
|
620
|
+
}
|
|
621
|
+
// Layout Charts Stacked Vertically
|
|
622
|
+
lines.push('### Automation Coverage');
|
|
623
|
+
lines.push(`<img src="${autoChartUrl}" width="100%">`);
|
|
624
|
+
lines.push('');
|
|
625
|
+
if (trendChartUrl) {
|
|
626
|
+
lines.push('### Trend of Test Cases');
|
|
627
|
+
lines.push(`<img src="${trendChartUrl}" width="100%">`);
|
|
628
|
+
lines.push('');
|
|
629
|
+
}
|
|
630
|
+
// 3. Top 5 Creators (HTML Table)
|
|
631
|
+
const creators = {};
|
|
632
|
+
for (const d of data) {
|
|
633
|
+
const creator = d.assignees[0] || 'Unassigned';
|
|
634
|
+
creators[creator] = (creators[creator] || 0) + 1;
|
|
635
|
+
}
|
|
636
|
+
const topCreators = Object.entries(creators).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
637
|
+
lines.push('### 🏆 Top 5 Test Case Creators');
|
|
638
|
+
lines.push('<table width="100%">');
|
|
639
|
+
lines.push(' <tr><th width="10%">#</th><th width="70%">Assignee</th><th width="20%">Count</th></tr>');
|
|
640
|
+
topCreators.forEach(([creator, count], idx) => {
|
|
641
|
+
lines.push(` <tr><td align="center">${idx + 1}</td><td><img src="https://github.com/${creator}.png?size=24" width="24" style="border-radius:50%; vertical-align:middle;"> <b>@${creator}</b></td><td align="center">${count}</td></tr>`);
|
|
642
|
+
});
|
|
643
|
+
lines.push('</table>');
|
|
644
|
+
lines.push('');
|
|
645
|
+
// 5. Test Cases List (HTML Table)
|
|
646
|
+
const priorityField = config.getReportMapping('priority') || 'attributes.custom_fields.priority';
|
|
647
|
+
const typeField = config.getReportMapping('type') || 'attributes.custom_fields.type';
|
|
648
|
+
const creatorField = config.getReportMapping('creator') || 'attributes.assignees[0]';
|
|
649
|
+
lines.push('### 📋 Test Cases included in this report');
|
|
650
|
+
lines.push('<table width="100%">');
|
|
651
|
+
lines.push(' <tr><th>ID</th><th>Title</th><th>Priority</th><th>Type</th><th>Updated</th><th>Assignee</th></tr>');
|
|
652
|
+
const sortedData = [...data].sort((a, b) => (b.updatedAt > a.updatedAt ? 1 : -1)).slice(0, 5);
|
|
653
|
+
for (const d of sortedData) {
|
|
654
|
+
const priority = resolvePath(d.originalResource, priorityField) || 'Medium';
|
|
655
|
+
const typeValue = resolvePath(d.originalResource, typeField) || 'General';
|
|
656
|
+
const creator = resolvePath(d.originalResource, creatorField) || d.assignees[0] || 'Unassigned';
|
|
657
|
+
const updatedStr = d.updatedAt ? d.updatedAt.replace('T', ' ').replace('Z', '') : 'N/A';
|
|
658
|
+
lines.push(` <tr>`);
|
|
659
|
+
lines.push(` <td><code>${d.id}</code></td>`);
|
|
660
|
+
lines.push(` <td>${d.title}</td>`);
|
|
661
|
+
lines.push(` <td align="center">${priority}</td>`);
|
|
662
|
+
lines.push(` <td align="center">${typeValue}</td>`);
|
|
663
|
+
lines.push(` <td align="center">${updatedStr.split(' ')[0]}</td>`);
|
|
664
|
+
lines.push(` <td align="center">@${creator}</td>`);
|
|
665
|
+
lines.push(` </tr>`);
|
|
666
|
+
}
|
|
667
|
+
lines.push('</table>');
|
|
668
|
+
}
|
|
669
|
+
else if (type === 'raw') {
|
|
670
|
+
lines.push('# Raw Data Export');
|
|
671
|
+
lines.push('> Note: Use --format json or --format csv for structured data.');
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
lines.push(`Unknown report type: ${type}`);
|
|
675
|
+
}
|
|
676
|
+
return lines.join('\n');
|
|
677
|
+
}
|
|
678
|
+
function groupByField(data, field) {
|
|
679
|
+
const result = {};
|
|
680
|
+
for (const item of data) {
|
|
681
|
+
let valStr = '';
|
|
682
|
+
if (field in item) {
|
|
683
|
+
valStr = String(item[field] || '');
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
valStr = String(resolvePath(item.originalResource, field) || '');
|
|
687
|
+
}
|
|
688
|
+
if (!result[valStr])
|
|
689
|
+
result[valStr] = [];
|
|
690
|
+
result[valStr].push(item);
|
|
691
|
+
}
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
function groupByTags(data) {
|
|
695
|
+
const result = {};
|
|
696
|
+
for (const item of data) {
|
|
697
|
+
for (const tag of item.labels) {
|
|
698
|
+
if (!result[tag])
|
|
699
|
+
result[tag] = [];
|
|
700
|
+
result[tag].push(item);
|
|
701
|
+
}
|
|
702
|
+
if (item.labels.length === 0) {
|
|
703
|
+
if (!result['untagged'])
|
|
704
|
+
result['untagged'] = [];
|
|
705
|
+
result['untagged'].push(item);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
function getStats(data) {
|
|
711
|
+
const stats = {
|
|
712
|
+
passed: 0, failed: 0, pending: 0, blocked: 0, skipped: 0, unexecuted: 0
|
|
713
|
+
};
|
|
714
|
+
for (const item of data) {
|
|
715
|
+
const s = item.status.toLowerCase();
|
|
716
|
+
if (stats[s] !== undefined) {
|
|
717
|
+
stats[s]++;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
stats[s] = 1;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return stats;
|
|
724
|
+
}
|