Skip to content

Commit 3b0bef7

Browse files
added csv formulas (#108)
1 parent b717b6c commit 3b0bef7

File tree

5 files changed

+177
-15
lines changed

5 files changed

+177
-15
lines changed

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ Our package bridges Cypress and Playwright test runs with Google Sheets or CSV,
1010
2. [Quick Start](#quick-start)
1111
- [Generate Reports in CSV Format](#generate-reports-in-csv-format)
1212
3. [Samples](#samples)
13-
- [Sheets Daily Report](#sheets-daily-report)
14-
- [Sheets Weekly Summary](#sheets-weekly-summary)
15-
- [Sheets Monthly Summary](#sheets-monthly-summary)
16-
- [CSV Daily Report](#csv-daily-report)
13+
- [Daily Report in Sheets](#daily-report-in-sheets)
14+
- [Weekly Summary in Sheets](#weekly-summary-in-sheets)
15+
- [Monthly Summary in Sheets](#monthly-summary-in-sheets)
16+
- [Daily Report in CSV](#daily-report-in-csv)
1717
4. [Sheets Setup Guide](#sheets-setup-guide)
1818
- [Cypress](#cypress)
1919
- [Playwright](#playwright)
@@ -87,25 +87,26 @@ Use either command to generate reports using either **NPX** or **NPM scripts** w
8787

8888
## Samples
8989

90-
### Sheets Daily Report
90+
### Daily Report in Sheets
9191

92-
The Daily Summary features high-level metrics such as a test quantity and percentage pass rate across multiple categories. Test details are organized into columns that include: the feature area, spec title, test name, test type, test category, team identifiers, test priority, test status, test state, manual case id, error details, and execution speed in format mm:ss:mmm.
92+
The Daily Summary features high-level metrics such as a test quantity and percentage pass rate across multiple categories. Test details are organized into columns that include: the feature area, spec title, test name, test type, test category, team identifiers, test priority, test status, test state, manual case id, error details, and speed of execution (in mm:ss:mmm format).
9393

9494
This structured format enables teams to quickly assess test outcomes on demand, with an example entry in the feature `area` '1-getting-started', in the 'todo' `spec`, the `test name` example to-do app can add new todo items', classified as `type` 'api', with no `category`, 'billing `team` is responsible, `status` 'passing' with a `speed` of '370 milliseconds'. The team may want to type in a `status` of 'billing team adding test category' with a `priority` of 'low'.
9595
![Screenshot of Feature](images/dailyReport.png)
9696

97-
### Sheets Weekly Summary
97+
### Weekly Summary in Sheets
9898

9999
The Weekly Summary is optional, and it provides a short term summary of the previous weeks `passed`, `failed`, `skipped`, and `total` test results. This summary is a great tool for teams who have weekly meetings to discuss their progress/status/gameplan
100100
![Screenshot of Feature](images/weeklySummary.png)
101101

102-
### Sheets Monthly Summary
102+
### Monthly Summary in Sheets
103103

104104
The Monthly Summary provides a brief overview of historical test execution results, giving long term visibility into `passed`, `failed`, `skipped`, and `total` test results for the previous month, allowing trends to emerge.
105105
![Screenshot of Feature](images/monthlySummary.png)
106106

107-
### CSV Daily Report
107+
### Daily Report in CSV
108108

109+
The Daily Report in CSV format delivers hardcoded snapshot of high-level test metrics, mirroring the formulaic insights provided by the Daily Summary in Sheets: feature area, spec title, test name, test type, test category, team identifiers, priority, status, state, manual case ID, error details, and speed of execution (in mm:ss:mmm format). Unlike the Sheets version, which leverages formulas for dynamic calculations, the CSV report contains hardcoded values, ensuring compatibility with static file formats while maintaining the same depth of analysis.
109110
![Screenshot of Feature](images/csvDailyReport.png)
110111

111112
## Sheets Setup Guide

images/csvDailyReport.png

-49.1 KB
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "qa-shadow-report",
3-
"version": "2.1.1",
3+
"version": "2.1.2",
44
"bin": {
55
"qa-shadow-report": "./cli.js",
66
"qasr": "./cli.js",

src/sharedMethods/csvHandler.js

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import chalk from 'chalk';
1010
* @returns {string} The CSV string.
1111
*/
1212
const arrayToCSV = (data) => {
13+
if (!Array.isArray(data) || !data.every((row) => Array.isArray(row))) {
14+
throw new Error('arrayToCSV expects a 2D array');
15+
}
1316
return data
1417
.map((row) =>
1518
row
@@ -50,5 +53,139 @@ export const saveCSV = (reportPayload, duplicate) => {
5053
}
5154

5255
fs.writeFileSync(filePath, csvData, 'utf8');
53-
console.log(`CSV file has been saved successfully to ${filePath}`);
56+
console.info(
57+
chalk.green(`CSV file has been saved successfully to ${filePath}`)
58+
);
59+
};
60+
61+
/**
62+
* Calculates daily summary metrics from test data, dynamically aggregating by unique TYPE, CATEGORY, TEAM values,
63+
* and aligns them in multiple rows. Ensures overall metrics are in columns 8-9. Handles empty/blank values efficiently.
64+
* @param {Array<Array<string>>} testData - Array of test data arrays (each with 12 elements).
65+
* @returns {string[][]} A 2D array representing the calculated header rows for CSV, aligned by columns.
66+
*/
67+
export const calculateDailySummaryMetrics = (testData) => {
68+
// Initialize counters
69+
const counters = {
70+
total: testData.length,
71+
passed: 0,
72+
failed: 0,
73+
skippedPending: 0,
74+
byType: {},
75+
byCategory: {},
76+
byTeam: {},
77+
};
78+
79+
// Helper to update counters for a given key and map
80+
const updateCounter = (key, map, status) => {
81+
if (!key) return; // Skip empty/blank keys
82+
if (!map[key]) map[key] = { passed: 0, total: 0 };
83+
map[key].total++;
84+
if (status.toLowerCase() === 'passed') map[key].passed++;
85+
};
86+
87+
// Process each test entry
88+
testData.forEach((row, index) => {
89+
const type = row[3] || '';
90+
const category = row[4] || '';
91+
const team = row[5] || '';
92+
const status = row[8] || '';
93+
94+
// Update overall counters
95+
if (status.toLowerCase() === 'passed') counters.passed++;
96+
if (status.toLowerCase() === 'failed') counters.failed++;
97+
if (status.toLowerCase() !== 'passed' && status.toLowerCase() !== 'failed')
98+
counters.skippedPending++;
99+
100+
// Aggregate by TYPE, CATEGORY, TEAM
101+
updateCounter(type, counters.byType, status);
102+
updateCounter(category, counters.byCategory, status);
103+
updateCounter(team, counters.byTeam, status);
104+
});
105+
106+
// Calculate percentages
107+
const calculatePercentage = (passed, total) =>
108+
total > 0 ? Math.round((passed / total) * 100) : 0;
109+
110+
// Helper to generate metric value
111+
const generateMetricValue = (passed, total) =>
112+
`${passed} of ${total} - (${calculatePercentage(passed, total)}%)`;
113+
114+
// Get metrics for each category
115+
const typeMetrics = Object.entries(counters.byType).map(
116+
([key, { passed, total }]) => [
117+
`# ${key} tests passed`,
118+
generateMetricValue(passed, total),
119+
]
120+
);
121+
const categoryMetrics = Object.entries(counters.byCategory).map(
122+
([key, { passed, total }]) => [
123+
`# ${key} tests passed`,
124+
generateMetricValue(passed, total),
125+
]
126+
);
127+
const teamMetrics = Object.entries(counters.byTeam).map(
128+
([key, { passed, total }]) => [
129+
`# ${key} tests passed`,
130+
generateMetricValue(passed, total),
131+
]
132+
);
133+
const overallMetrics = [
134+
`# passed tests`,
135+
`${counters.passed} (${calculatePercentage(counters.passed, counters.total)}%)`,
136+
`# failed tests`,
137+
`${counters.failed} (${calculatePercentage(counters.failed, counters.total)}%)`,
138+
`# skipped/pending tests`,
139+
`${counters.skippedPending} (${calculatePercentage(counters.skippedPending, counters.total)}%)`,
140+
`# total tests`,
141+
`${counters.total}`,
142+
];
143+
144+
// Determine the number of rows (at least 4 for overall metrics)
145+
const maxUniqueMetrics = Math.max(
146+
typeMetrics.length,
147+
categoryMetrics.length,
148+
teamMetrics.length
149+
);
150+
const minRows = 4; // Ensure at least 4 rows for overall metrics
151+
const maxRows = Math.max(maxUniqueMetrics, minRows);
152+
153+
// Build the 2D header with dynamically determined rows
154+
const headerRows = [
155+
// Initial empty row
156+
Array(12).fill(''),
157+
];
158+
159+
for (let i = 0; i < maxRows; i++) {
160+
const row = [];
161+
// Columns 1-2: TYPE metric (if available)
162+
row.push(typeMetrics[i]?.[0] || '', typeMetrics[i]?.[1] || '');
163+
// Columns 3-4: CATEGORY metric (if available)
164+
row.push(categoryMetrics[i]?.[0] || '', categoryMetrics[i]?.[1] || '');
165+
// Columns 5-6: TEAM metric (if available)
166+
row.push(teamMetrics[i]?.[0] || '', teamMetrics[i]?.[1] || '');
167+
// Column 7: Placeholder
168+
row.push('');
169+
// Columns 8-9: Overall metrics (fixed to first 4 rows)
170+
if (i === 0) {
171+
row.push(overallMetrics[0] || '', overallMetrics[1] || '');
172+
} else if (i === 1) {
173+
row.push(overallMetrics[2] || '', overallMetrics[3] || '');
174+
} else if (i === 2) {
175+
row.push(overallMetrics[4] || '', overallMetrics[5] || '');
176+
} else if (i === 3) {
177+
row.push(overallMetrics[6] || '', overallMetrics[7] || '');
178+
} else {
179+
row.push('', '');
180+
}
181+
// Columns 10-12: Placeholders
182+
row.push('', '', '');
183+
184+
headerRows.push(row);
185+
}
186+
187+
// Add final empty row
188+
headerRows.push(Array(12).fill(''));
189+
190+
return headerRows;
54191
};

src/sharedMethods/dailyReportHandler.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
setTextWrappingToClip,
2020
} from './styles.js';
2121
import { TEST_DATA } from '../../constants.js';
22-
import { saveCSV } from './csvHandler.js';
22+
import { calculateDailySummaryMetrics, saveCSV } from './csvHandler.js';
2323
import { doesTodaysReportExist } from './dailyReportRequired.js';
2424
import { transformPlaywrightToFriendlyFormat } from './convertPayloads.js';
2525
import chalk from 'chalk';
@@ -59,11 +59,35 @@ export const handleDailyReport = async ({
5959
}
6060
const fullDailyPayload = await buildDailyPayload(testPayload, playwright);
6161
if (csv) {
62-
const reportPayload = [
62+
const summaryHeader = calculateDailySummaryMetrics(
63+
fullDailyPayload.bodyPayload
64+
);
65+
// Ensure the original header is a 1D array
66+
const originalHeader = Array.isArray(
6367
fullDailyPayload.headerPayload[
6468
fullDailyPayload.headerPayload.length - 1
65-
],
66-
...fullDailyPayload.bodyPayload,
69+
]
70+
)
71+
? fullDailyPayload.headerPayload[
72+
fullDailyPayload.headerPayload.length - 1
73+
]
74+
: [
75+
fullDailyPayload.headerPayload[
76+
fullDailyPayload.headerPayload.length - 1
77+
],
78+
];
79+
80+
// Pad the original header to match body row length (12 columns)
81+
const bodyRowLength = fullDailyPayload.bodyPayload[0]?.length || 12;
82+
const paddedOriginalHeader = [
83+
...originalHeader,
84+
...Array(Math.max(0, bodyRowLength - originalHeader.length)).fill(''),
85+
];
86+
87+
const reportPayload = [
88+
...summaryHeader, // Multi-row header, each row is 12 columns
89+
paddedOriginalHeader, // Column names, padded to 12 columns
90+
...fullDailyPayload.bodyPayload, // Already 2D, each row is 12 columns
6791
];
6892
saveCSV(reportPayload, duplicate);
6993
} else {

0 commit comments

Comments
 (0)