@arghajit/playwright-pulse-report 0.2.0 → 0.2.1

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.
@@ -0,0 +1,165 @@
1
+ import * as fs from "fs/promises";
2
+ import path from "path";
3
+ // XLSX is NO LONGER NEEDED here
4
+
5
+ // Use dynamic import for chalk as it's ESM only for prettier console logs
6
+ let chalk;
7
+ try {
8
+ chalk = (await import("chalk")).default;
9
+ } catch (e) {
10
+ chalk = {
11
+ green: (t) => t,
12
+ red: (t) => t,
13
+ yellow: (t) => t,
14
+ blue: (t) => t,
15
+ bold: (t) => t,
16
+ };
17
+ }
18
+
19
+ const DEFAULT_OUTPUT_DIR = "pulse-report";
20
+ const CURRENT_RUN_JSON_FILE = "playwright-pulse-report.json"; // Source of the current run data
21
+ const HISTORY_SUBDIR = "history"; // Subdirectory for historical JSON files
22
+ const HISTORY_FILE_PREFIX = "trend-";
23
+ const MAX_HISTORY_FILES = 15; // Store last 15 runs
24
+
25
+ async function archiveCurrentRunData() {
26
+ const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
27
+ const currentRunJsonPath = path.join(outputDir, CURRENT_RUN_JSON_FILE);
28
+ const historyDir = path.join(outputDir, HISTORY_SUBDIR);
29
+
30
+ try {
31
+ // 1. Ensure history directory exists
32
+ await fs.mkdir(historyDir, { recursive: true });
33
+ // console.log(chalk.blue(`History directory ensured at: ${historyDir}`));
34
+
35
+ // 2. Read the current run's JSON data
36
+ // console.log(chalk.blue(`Reading current run data from: ${currentRunJsonPath}`));
37
+ let currentReportData;
38
+ try {
39
+ const jsonData = await fs.readFile(currentRunJsonPath, "utf-8");
40
+ currentReportData = JSON.parse(jsonData);
41
+ if (
42
+ !currentReportData ||
43
+ !currentReportData.run ||
44
+ !currentReportData.run.timestamp
45
+ ) {
46
+ throw new Error(
47
+ "Invalid current run JSON report structure. Missing 'run' or 'run.timestamp' data."
48
+ );
49
+ }
50
+ } catch (error) {
51
+ console.error(
52
+ chalk.red(
53
+ `Error reading or parsing current run JSON report at ${currentRunJsonPath}: ${error.message}`
54
+ )
55
+ );
56
+ process.exit(1); // Exit if we can't read the source file
57
+ }
58
+
59
+ // 3. Determine the filename for the new history file
60
+ // Ensure timestamp is a valid number before using getTime()
61
+ let runTimestampMs;
62
+ try {
63
+ runTimestampMs = new Date(currentReportData.run.timestamp).getTime();
64
+ if (isNaN(runTimestampMs)) {
65
+ throw new Error(
66
+ `Invalid timestamp value: ${currentReportData.run.timestamp}`
67
+ );
68
+ }
69
+ } catch (dateError) {
70
+ console.error(
71
+ chalk.red(
72
+ `Failed to parse timestamp '${currentReportData.run.timestamp}': ${dateError.message}`
73
+ )
74
+ );
75
+ process.exit(1);
76
+ }
77
+
78
+ const newHistoryFileName = `${HISTORY_FILE_PREFIX}${runTimestampMs}.json`;
79
+ const newHistoryFilePath = path.join(historyDir, newHistoryFileName);
80
+
81
+ // 4. Write the current run's data to the new history file
82
+ // console.log(chalk.blue(`Saving current run data to: ${newHistoryFilePath}`));
83
+ await fs.writeFile(
84
+ newHistoryFilePath,
85
+ JSON.stringify(currentReportData, null, 2),
86
+ "utf-8"
87
+ );
88
+ console.log(chalk.green(`Archived current run to: ${newHistoryFilePath}`));
89
+
90
+ // 5. Prune old history files
91
+ await pruneOldHistoryFiles(historyDir);
92
+ } catch (error) {
93
+ console.error(
94
+ chalk.red(`Error in archiveCurrentRunData: ${error.message}`)
95
+ );
96
+ // console.error(error.stack); // Uncomment for more detailed stack trace
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ async function pruneOldHistoryFiles(historyDir) {
102
+ // console.log(chalk.blue(`Pruning old history files in ${historyDir} (keeping last ${MAX_HISTORY_FILES})...`));
103
+ try {
104
+ const files = await fs.readdir(historyDir);
105
+ const historyJsonFiles = files
106
+ .filter(
107
+ (file) => file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json")
108
+ )
109
+ .map((file) => {
110
+ const timestampPart = file
111
+ .replace(HISTORY_FILE_PREFIX, "")
112
+ .replace(".json", "");
113
+ return { name: file, timestamp: parseInt(timestampPart, 10) };
114
+ })
115
+ .filter((file) => !isNaN(file.timestamp))
116
+ .sort((a, b) => a.timestamp - b.timestamp); // Sort ascending (oldest first)
117
+
118
+ if (historyJsonFiles.length > MAX_HISTORY_FILES) {
119
+ const filesToDelete = historyJsonFiles.slice(
120
+ 0,
121
+ historyJsonFiles.length - MAX_HISTORY_FILES
122
+ );
123
+ console.log(
124
+ chalk.yellow(
125
+ `Found ${historyJsonFiles.length} history files. Pruning ${filesToDelete.length} oldest file(s)...`
126
+ )
127
+ );
128
+ for (const fileMeta of filesToDelete) {
129
+ const filePathToDelete = path.join(historyDir, fileMeta.name);
130
+ try {
131
+ await fs.unlink(filePathToDelete);
132
+ // console.log(chalk.gray(`Deleted old history file: ${fileMeta.name}`));
133
+ } catch (deleteError) {
134
+ console.warn(
135
+ chalk.yellow(
136
+ `Could not delete old history file ${fileMeta.name}: ${deleteError.message}`
137
+ )
138
+ );
139
+ }
140
+ }
141
+ } else {
142
+ // console.log(chalk.green(`Found ${historyJsonFiles.length} history files. No pruning needed.`));
143
+ }
144
+ } catch (error) {
145
+ console.warn(
146
+ chalk.yellow(
147
+ `Warning during history pruning in ${historyDir}: ${error.message}`
148
+ )
149
+ );
150
+ // Don't exit for pruning errors, as saving the current run is more critical
151
+ }
152
+ }
153
+
154
+ // Main execution
155
+ archiveCurrentRunData().catch((error) => {
156
+ // Fallback catch, though critical errors in archiveCurrentRunData should exit
157
+ if (process.exitCode === undefined || process.exitCode === 0) {
158
+ // check if not already exited
159
+ console.error(
160
+ chalk.red.bold("An unexpected error occurred in history archiving:"),
161
+ error
162
+ );
163
+ process.exit(1);
164
+ }
165
+ });
@@ -1,273 +0,0 @@
1
- // generate-trend-excel.mjs
2
- import * as fs from "fs/promises";
3
- import path from "path";
4
- import * as XLSX from "xlsx";
5
-
6
- // Use dynamic import for chalk as it's ESM only for prettier console logs
7
- let chalk;
8
- try {
9
- chalk = (await import("chalk")).default;
10
- } catch (e) {
11
- chalk = { green: (t) => t, red: (t) => t, yellow: (t) => t, blue: (t) => t }; // Basic fallback
12
- }
13
-
14
- const DEFAULT_OUTPUT_DIR = "pulse-report"; // Should match reporter's outputDir
15
- const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
16
- const TREND_EXCEL_FILE_NAME = "trend.xls";
17
-
18
- class ExcelTrendManager {
19
- _excelFilePath;
20
- _maxRuns = 5;
21
-
22
- constructor(outputDir, excelFileName = TREND_EXCEL_FILE_NAME) {
23
- this._excelFilePath = path.join(outputDir, excelFileName);
24
- }
25
-
26
- getExcelFilePath() {
27
- return this._excelFilePath;
28
- }
29
-
30
- async _readExistingData() {
31
- try {
32
- await fs.access(this._excelFilePath);
33
- const buffer = await fs.readFile(this._excelFilePath);
34
- return XLSX.read(buffer, { type: "buffer" });
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- _shiftOverallRuns(data, currentNumericRunId) {
41
- const validData = Array.isArray(data) ? data : [];
42
- const pastOrCurrentData = validData.filter(
43
- (row) =>
44
- row.hasOwnProperty("RUN_ID") &&
45
- typeof row.RUN_ID === "number" &&
46
- row.RUN_ID <= currentNumericRunId
47
- );
48
- const sortedData = [...pastOrCurrentData].sort(
49
- (a, b) => a.RUN_ID - b.RUN_ID
50
- );
51
- if (sortedData.length > this._maxRuns) {
52
- return sortedData.slice(sortedData.length - this._maxRuns);
53
- }
54
- return sortedData;
55
- }
56
-
57
- async updateTrendData(
58
- runIdFromReport,
59
- timestamp,
60
- totalTests,
61
- passed,
62
- failed,
63
- skipped,
64
- duration,
65
- testResultsForThisRun
66
- ) {
67
- let workbook = await this._readExistingData();
68
- const numericRunId = Math.floor(timestamp / 1000);
69
-
70
- if (!workbook) {
71
- workbook = XLSX.utils.book_new();
72
- // If the workbook is new, SheetNames will be empty.
73
- // We need to initialize it if it doesn't exist
74
- if (!workbook.SheetNames) {
75
- workbook.SheetNames = [];
76
- }
77
- } else {
78
- // Ensure SheetNames exists even for existing workbooks (should, but defensive)
79
- if (!workbook.SheetNames) {
80
- workbook.SheetNames = [];
81
- }
82
- }
83
-
84
- // --- Overall Data ---
85
- let existingOverallData = [];
86
- if (workbook.Sheets["overall"]) {
87
- try {
88
- existingOverallData = XLSX.utils.sheet_to_json(
89
- workbook.Sheets["overall"]
90
- );
91
- } catch (e) {
92
- console.warn(
93
- chalk.yellow(
94
- "Could not parse existing 'overall' sheet. Starting fresh. Error:",
95
- e.message
96
- )
97
- );
98
- existingOverallData = [];
99
- }
100
- }
101
- existingOverallData = existingOverallData.filter(
102
- (row) => row.RUN_ID !== numericRunId
103
- );
104
- const newOverallRow = {
105
- RUN_ID: numericRunId,
106
- DURATION: duration,
107
- TIMESTAMP: timestamp,
108
- TOTAL_TESTS: totalTests,
109
- PASSED: passed,
110
- FAILED: failed,
111
- SKIPPED: skipped,
112
- };
113
- let updatedOverallData = [...existingOverallData, newOverallRow];
114
- updatedOverallData = this._shiftOverallRuns(
115
- updatedOverallData,
116
- numericRunId
117
- );
118
-
119
- const overallSheet = XLSX.utils.json_to_sheet(updatedOverallData);
120
-
121
- // UPDATED: Use book_append_sheet for new sheets, or replace existing
122
- if (!workbook.SheetNames.includes("overall")) {
123
- XLSX.utils.book_append_sheet(workbook, overallSheet, "overall");
124
- // Move "overall" to the beginning if it was just added and not already first
125
- const overallIndex = workbook.SheetNames.indexOf("overall");
126
- if (overallIndex > 0) {
127
- const sheetName = workbook.SheetNames.splice(overallIndex, 1)[0];
128
- workbook.SheetNames.unshift(sheetName);
129
- }
130
- } else {
131
- workbook.Sheets["overall"] = overallSheet; // Replace existing
132
- }
133
- XLSX.utils.book_set_sheet_visibility(workbook, "overall", 0);
134
-
135
- // --- Per-Test Data Sheet for the Current Run ---
136
- const runKey = `test run ${numericRunId}`;
137
- const currentRunTestData = testResultsForThisRun.map((test) => ({
138
- TEST_NAME: test.name,
139
- DURATION: test.duration,
140
- STATUS: test.status,
141
- TIMESTAMP: timestamp,
142
- }));
143
- const testRunSheet = XLSX.utils.json_to_sheet(currentRunTestData);
144
-
145
- // UPDATED: Logic to add or replace the sheet and ensure it's in SheetNames
146
- if (!workbook.SheetNames.includes(runKey)) {
147
- XLSX.utils.book_append_sheet(workbook, testRunSheet, runKey); // This adds to Sheets and SheetNames
148
- } else {
149
- workbook.Sheets[runKey] = testRunSheet; // Just replace the sheet data
150
- }
151
- // Now that the sheet is guaranteed to be in SheetNames and workbook.Sheets, set visibility
152
- XLSX.utils.book_set_sheet_visibility(workbook, runKey, 0);
153
-
154
- // --- Maintain Max Sheet Count for Individual Test Runs ---
155
- let testRunSheetNames = workbook.SheetNames.filter(
156
- (name) => name.toLowerCase().startsWith("test run ") && name !== "overall"
157
- );
158
- testRunSheetNames.sort((a, b) => {
159
- const matchA = a.match(/test run (\d+)$/i);
160
- const matchB = b.match(/test run (\d+)$/i);
161
- const idA = matchA && matchA[1] ? parseInt(matchA[1], 10) : 0;
162
- const idB = matchB && matchB[1] ? parseInt(matchB[1], 10) : 0;
163
- return idA - idB;
164
- });
165
-
166
- if (testRunSheetNames.length > this._maxRuns) {
167
- const sheetsToRemoveCount = testRunSheetNames.length - this._maxRuns;
168
- const removedSheetNames = [];
169
- for (let i = 0; i < sheetsToRemoveCount; i++) {
170
- const oldestSheetName = testRunSheetNames[i];
171
- // Remove from workbook.Sheets
172
- delete workbook.Sheets[oldestSheetName];
173
- removedSheetNames.push(oldestSheetName);
174
- }
175
- // Rebuild SheetNames array without the removed sheets
176
- workbook.SheetNames = workbook.SheetNames.filter(
177
- (name) => !removedSheetNames.includes(name)
178
- );
179
- }
180
-
181
- // --- Write Workbook ---
182
- try {
183
- const buffer = XLSX.write(workbook, { bookType: "xls", type: "buffer" });
184
- await fs.writeFile(this._excelFilePath, buffer);
185
- console.log(
186
- chalk.green(
187
- `Excel trend report updated successfully at ${this._excelFilePath}`
188
- )
189
- );
190
- } catch (writeError) {
191
- console.error(
192
- chalk.red(`Failed to write Excel file at ${this._excelFilePath}`)
193
- );
194
- console.error(chalk.red("Write Error Details:"), writeError);
195
- throw writeError;
196
- }
197
- }
198
- }
199
-
200
- async function generateTrendExcel() {
201
- const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
202
- const jsonReportPath = path.join(outputDir, DEFAULT_JSON_FILE);
203
-
204
- // Ensure output directory exists before any file operations
205
- try {
206
- await fs.mkdir(outputDir, { recursive: true });
207
- } catch (mkdirError) {
208
- console.error(
209
- chalk.red(`Failed to create output directory ${outputDir}:`),
210
- mkdirError
211
- );
212
- process.exit(1);
213
- }
214
-
215
- console.log(chalk.blue(`Reading JSON report from: ${jsonReportPath}`));
216
- let reportData;
217
- try {
218
- const jsonData = await fs.readFile(jsonReportPath, "utf-8");
219
- reportData = JSON.parse(jsonData);
220
- if (!reportData || !reportData.run || !Array.isArray(reportData.results)) {
221
- throw new Error(
222
- "Invalid JSON report structure. Missing 'run' or 'results' data."
223
- );
224
- }
225
- } catch (error) {
226
- console.error(
227
- chalk.red(`Error reading or parsing JSON report: ${error.message}`)
228
- );
229
- console.error(chalk.red("JSON Read/Parse Error Details:"), error);
230
- process.exit(1);
231
- }
232
-
233
- const { run, results } = reportData;
234
- if (!run.timestamp || isNaN(new Date(run.timestamp).getTime())) {
235
- console.error(
236
- chalk.red(`Invalid or missing run.timestamp in JSON: ${run.timestamp}`)
237
- );
238
- process.exit(1);
239
- }
240
- const runTimestamp = new Date(run.timestamp).getTime();
241
-
242
- const testResultsForExcel = results.map((r) => ({
243
- name: r.name,
244
- duration: r.duration,
245
- status: r.status,
246
- }));
247
-
248
- const excelManager = new ExcelTrendManager(outputDir);
249
- try {
250
- await excelManager.updateTrendData(
251
- run.id,
252
- runTimestamp,
253
- run.totalTests,
254
- run.passed,
255
- run.failed,
256
- run.skipped,
257
- run.duration,
258
- testResultsForExcel
259
- );
260
- } catch (excelError) {
261
- console.error(chalk.red("Aborting due to error during Excel generation."));
262
- console.error(chalk.red("Excel Generation Error Details:"), excelError);
263
- process.exit(1);
264
- }
265
- }
266
-
267
- generateTrendExcel().catch((error) => {
268
- console.error(
269
- chalk.red("An unexpected error occurred in generate-trend-excel:"),
270
- error
271
- );
272
- process.exit(1);
273
- });