@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.
package/README.md CHANGED
@@ -227,20 +227,21 @@ playwright-pulse-reporter/
227
227
  │ └── app/ # Next.js dashboard
228
228
  ├── scripts/
229
229
  │ └── generate-static-report.mjs # HTML generator
230
+ | └── generate-trend.mjs # Generate Trends
230
231
  | └── merge-pulse-report.mjs # merge sharded reports
231
232
  | └── sendReport.mjs # Send email report
232
233
  ├── pulse-report/ # Generated reports
233
234
  └── sample-report.json # Example data
234
235
  ```
235
236
 
236
- ## 🎉 What's New in v0.2.0
237
+ ## 🎉 What's New in v0.2.1
237
238
 
238
239
  ### ✨ **Key Improvements**
239
240
 
240
241
  | Feature | Description |
241
242
  |---------|-------------|
242
243
  | **🎨 Refined UI** | Completely redesigned static HTML reports for better readability and navigation |
243
- | **📊 History Trends** | Visual analytics for:<br>• Test History for last 5 runs<br>• Test suite pass/fail rates<br>• Duration trends<br>• Individual test flakiness |
244
+ | **📊 History Trends** | Visual analytics for:<br>• Test History for last 15 runs<br>• Test suite pass/fail rates<br>• Duration trends<br>• Individual test flakiness |
244
245
  | **🛠️ Project Fixes** | Corrected project name display in test suite components |
245
246
 
246
247
  ### 🚀 **Upgrade Now**
@@ -41,7 +41,7 @@ const crypto_1 = require("crypto");
41
41
  const attachment_utils_1 = require("./attachment-utils"); // Use relative path
42
42
  const convertStatus = (status, testCase) => {
43
43
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
44
- return status === "failed" ? "failed" : "failed";
44
+ return "failed";
45
45
  }
46
46
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "skipped") {
47
47
  return "skipped";
@@ -94,9 +94,12 @@ class PlaywrightPulseReporter {
94
94
  : undefined;
95
95
  this._ensureDirExists(this.outputDir)
96
96
  .then(() => {
97
- if (this.shardIndex === undefined) {
97
+ if (this.shardIndex === undefined || this.shardIndex === 0) {
98
98
  console.log(`PlaywrightPulseReporter: Starting test run with ${suite.allTests().length} tests${this.isSharded ? ` across ${totalShards} shards` : ""}. Pulse outputting to ${this.outputDir}`);
99
- return this._cleanupTemporaryFiles();
99
+ if (this.shardIndex === undefined ||
100
+ (this.isSharded && this.shardIndex === 0)) {
101
+ return this._cleanupTemporaryFiles();
102
+ }
100
103
  }
101
104
  })
102
105
  .catch((err) => console.error("Pulse Reporter: Error during initialization:", err));
@@ -104,14 +107,12 @@ class PlaywrightPulseReporter {
104
107
  onTestBegin(test) {
105
108
  // console.log(`Starting test: ${test.title}`);
106
109
  }
107
- async processStep(step, testId, browserName, // Changed from browserName for clarity
108
- testCase) {
110
+ async processStep(step, testId, browserName, testCase) {
109
111
  var _a, _b, _c, _d;
110
112
  let stepStatus = "passed";
111
113
  let errorMessage = ((_a = step.error) === null || _a === void 0 ? void 0 : _a.message) || undefined;
112
114
  if ((_c = (_b = step.error) === null || _b === void 0 ? void 0 : _b.message) === null || _c === void 0 ? void 0 : _c.startsWith("Test is skipped:")) {
113
115
  stepStatus = "skipped";
114
- errorMessage = "Info: Test is skipped:";
115
116
  }
116
117
  else {
117
118
  stepStatus = convertStatus(step.error ? "failed" : "passed", testCase);
@@ -124,28 +125,6 @@ class PlaywrightPulseReporter {
124
125
  codeLocation = `${path.relative(this.config.rootDir, step.location.file)}:${step.location.line}:${step.location.column}`;
125
126
  }
126
127
  let stepTitle = step.title;
127
- // This logic had a 'status' variable that was not defined in this scope.
128
- // Assuming it meant to check 'stepStatus' or 'testCase.expectedStatus' related to step.error.
129
- // Corrected to reflect comparison with testCase if step.category is 'test'.
130
- if (step.category === "test" && testCase) {
131
- // If a test step (not a hook) resulted in an error, but the test was expected to fail,
132
- // this specific logic might need refinement based on how you want to report step errors
133
- // within a test that is expected to fail.
134
- // The current convertStatus handles the overall testCase expectedStatus.
135
- // For step-specific error messages when testCase.expectedStatus === 'failed':
136
- if (testCase.expectedStatus === "failed") {
137
- if (step.error) {
138
- // If the step itself has an error
139
- // errorMessage is already set from step.error.message
140
- }
141
- else {
142
- // If a step within an expected-to-fail test passes, it's usually not an error for the step itself.
143
- }
144
- }
145
- else if (testCase.expectedStatus === "skipped") {
146
- // errorMessage is already set if step.error.message started with "Test is skipped:"
147
- }
148
- }
149
128
  return {
150
129
  id: `${testId}_step_${startTime.toISOString()}-${duration}-${(0, crypto_1.randomUUID)()}`,
151
130
  title: stepTitle,
@@ -169,10 +148,7 @@ class PlaywrightPulseReporter {
169
148
  async onTestEnd(test, result) {
170
149
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
171
150
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
172
- // Use project.name for a user-friendly display name
173
- const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || "unknown";
174
- // If you need the engine name (chromium, firefox, webkit)
175
- // const browserEngineName = project?.use?.browserName || "unknown_engine";
151
+ const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || (project === null || project === void 0 ? void 0 : project.name) || "unknown";
176
152
  const testStatus = convertStatus(result.status, test);
177
153
  const startTime = new Date(result.startTime);
178
154
  const endTime = new Date(startTime.getTime() + result.duration);
@@ -181,14 +157,10 @@ class PlaywrightPulseReporter {
181
157
  .titlePath()
182
158
  .join("_")
183
159
  .replace(/[^a-zA-Z0-9]/g, "_")}_${startTime.getTime()}`;
184
- const processAllSteps = async (steps
185
- // parentTestStatus parameter was not used, removed for now.
186
- // If needed for inherited status logic for steps, it can be re-added.
187
- ) => {
160
+ const processAllSteps = async (steps) => {
188
161
  let processed = [];
189
162
  for (const step of steps) {
190
- const processedStep = await this.processStep(step, testIdForFiles, browserName, // Pass display name
191
- test);
163
+ const processedStep = await this.processStep(step, testIdForFiles, browserName, test);
192
164
  processed.push(processedStep);
193
165
  if (step.steps && step.steps.length > 0) {
194
166
  processedStep.steps = await processAllSteps(step.steps);
@@ -206,69 +178,56 @@ class PlaywrightPulseReporter {
206
178
  catch (e) {
207
179
  console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
208
180
  }
209
- // --- Capture stdout and stderr ---
210
181
  const stdoutMessages = [];
211
182
  if (result.stdout && result.stdout.length > 0) {
212
183
  result.stdout.forEach((item) => {
213
- if (typeof item === "string") {
214
- stdoutMessages.push(item);
215
- }
216
- else {
217
- // If item is not a string, Playwright's typings indicate it's a Buffer (or Buffer-like).
218
- // We must call toString() on it.
219
- // The 'item' here is typed as 'Buffer' from the 'else' branch of '(string | Buffer)[]'
220
- stdoutMessages.push(item.toString());
221
- }
184
+ stdoutMessages.push(typeof item === "string" ? item : item.toString());
222
185
  });
223
186
  }
224
187
  const stderrMessages = [];
225
188
  if (result.stderr && result.stderr.length > 0) {
226
189
  result.stderr.forEach((item) => {
227
- if (typeof item === "string") {
228
- stderrMessages.push(item);
229
- }
230
- else {
231
- // If item is not a string, Playwright's typings indicate it's a Buffer (or Buffer-like).
232
- // We must call toString() on it.
233
- stderrMessages.push(item.toString());
234
- }
190
+ stderrMessages.push(typeof item === "string" ? item : item.toString());
235
191
  });
236
192
  }
237
- // --- End capture stdout and stderr ---
193
+ const uniqueTestId = test.id;
238
194
  const pulseResult = {
239
- id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`,
195
+ id: uniqueTestId,
240
196
  runId: "TBD",
241
197
  name: test.titlePath().join(" > "),
242
- // Use project.name for suiteName if desired, or fallback
243
198
  suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_f = this.config.projects[0]) === null || _f === void 0 ? void 0 : _f.name) || "Default Suite",
244
199
  status: testStatus,
245
200
  duration: result.duration,
246
201
  startTime: startTime,
247
202
  endTime: endTime,
248
- browser: browserName, // Use the user-friendly project name
203
+ browser: browserName,
249
204
  retries: result.retry,
250
205
  steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length) ? await processAllSteps(result.steps) : [],
251
206
  errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
252
207
  stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
253
208
  codeSnippet: codeSnippet,
254
209
  tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
255
- screenshots: [], // Will be populated by attachFiles
210
+ screenshots: [],
256
211
  videoPath: undefined,
257
212
  tracePath: undefined,
258
- // videoPath and tracePath might be deprecated if using the array versions above
259
- // Depending on attachFiles implementation
260
- // Add the captured console messages
261
213
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
262
214
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
263
215
  };
264
216
  try {
265
- // Pass this.options which should contain the resolved outputDir
266
217
  (0, attachment_utils_1.attachFiles)(testIdForFiles, result, pulseResult, this.options);
267
218
  }
268
219
  catch (attachError) {
269
220
  console.error(`Pulse Reporter: Error processing attachments for test ${pulseResult.name} (ID: ${testIdForFiles}): ${attachError.message}`);
270
221
  }
271
- this.results.push(pulseResult);
222
+ const existingTestIndex = this.results.findIndex((r) => r.id === uniqueTestId);
223
+ if (existingTestIndex !== -1) {
224
+ if (pulseResult.retries >= this.results[existingTestIndex].retries) {
225
+ this.results[existingTestIndex] = pulseResult;
226
+ }
227
+ }
228
+ else {
229
+ this.results.push(pulseResult);
230
+ }
272
231
  }
273
232
  onError(error) {
274
233
  var _a;
@@ -279,7 +238,6 @@ class PlaywrightPulseReporter {
279
238
  }
280
239
  async _writeShardResults() {
281
240
  if (this.shardIndex === undefined) {
282
- // console.warn("Pulse Reporter: _writeShardResults called unexpectedly in main process. Skipping.");
283
241
  return;
284
242
  }
285
243
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${this.shardIndex}.json`);
@@ -291,29 +249,38 @@ class PlaywrightPulseReporter {
291
249
  }
292
250
  }
293
251
  async _mergeShardResults(finalRunData) {
294
- let allResults = [];
252
+ let allShardProcessedResults = [];
295
253
  const totalShards = this.config.shard ? this.config.shard.total : 1;
296
254
  for (let i = 0; i < totalShards; i++) {
297
255
  const tempFilePath = path.join(this.outputDir, `${TEMP_SHARD_FILE_PREFIX}${i}.json`);
298
256
  try {
299
257
  const content = await fs.readFile(tempFilePath, "utf-8");
300
258
  const shardResults = JSON.parse(content);
301
- shardResults.forEach((r) => (r.runId = finalRunData.id));
302
- allResults = allResults.concat(shardResults);
259
+ allShardProcessedResults =
260
+ allShardProcessedResults.concat(shardResults);
303
261
  }
304
262
  catch (error) {
305
263
  if ((error === null || error === void 0 ? void 0 : error.code) === "ENOENT") {
306
- console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}.`);
264
+ console.warn(`Pulse Reporter: Shard results file not found: ${tempFilePath}. This might be normal if a shard had no tests or failed early.`);
307
265
  }
308
266
  else {
309
267
  console.error(`Pulse Reporter: Could not read/parse results from shard ${i} (${tempFilePath}). Error:`, error);
310
268
  }
311
269
  }
312
270
  }
313
- finalRunData.passed = allResults.filter((r) => r.status === "passed").length;
314
- finalRunData.failed = allResults.filter((r) => r.status === "failed").length;
315
- finalRunData.skipped = allResults.filter((r) => r.status === "skipped").length;
316
- finalRunData.totalTests = allResults.length;
271
+ let finalUniqueResultsMap = new Map();
272
+ for (const result of allShardProcessedResults) {
273
+ const existing = finalUniqueResultsMap.get(result.id);
274
+ if (!existing || result.retries >= existing.retries) {
275
+ finalUniqueResultsMap.set(result.id, result);
276
+ }
277
+ }
278
+ const finalResultsList = Array.from(finalUniqueResultsMap.values());
279
+ finalResultsList.forEach((r) => (r.runId = finalRunData.id));
280
+ finalRunData.passed = finalResultsList.filter((r) => r.status === "passed").length;
281
+ finalRunData.failed = finalResultsList.filter((r) => r.status === "failed").length;
282
+ finalRunData.skipped = finalResultsList.filter((r) => r.status === "skipped").length;
283
+ finalRunData.totalTests = finalResultsList.length;
317
284
  const reviveDates = (key, value) => {
318
285
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
319
286
  if (typeof value === "string" && isoDateRegex.test(value)) {
@@ -322,10 +289,10 @@ class PlaywrightPulseReporter {
322
289
  }
323
290
  return value;
324
291
  };
325
- const finalParsedResults = JSON.parse(JSON.stringify(allResults), reviveDates);
292
+ const properlyTypedResults = JSON.parse(JSON.stringify(finalResultsList), reviveDates);
326
293
  return {
327
294
  run: finalRunData,
328
- results: finalParsedResults,
295
+ results: properlyTypedResults,
329
296
  metadata: { generatedAt: new Date().toISOString() },
330
297
  };
331
298
  }
@@ -339,12 +306,11 @@ class PlaywrightPulseReporter {
339
306
  }
340
307
  catch (error) {
341
308
  if ((error === null || error === void 0 ? void 0 : error.code) !== "ENOENT") {
342
- console.error("Pulse Reporter: Error cleaning up temporary files:", error);
309
+ console.warn("Pulse Reporter: Warning during cleanup of temporary files:", error.message);
343
310
  }
344
311
  }
345
312
  }
346
313
  async _ensureDirExists(dirPath) {
347
- // Removed 'clean' parameter as it was unused
348
314
  try {
349
315
  await fs.mkdir(dirPath, { recursive: true });
350
316
  }
@@ -356,14 +322,13 @@ class PlaywrightPulseReporter {
356
322
  }
357
323
  }
358
324
  async onEnd(result) {
359
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
325
+ var _a, _b, _c;
360
326
  if (this.shardIndex !== undefined) {
361
327
  await this._writeShardResults();
362
328
  return;
363
329
  }
364
330
  const runEndTime = Date.now();
365
331
  const duration = runEndTime - this.runStartTime;
366
- // Consider making the UUID part truly random for each run if this ID needs to be globally unique over time
367
332
  const runId = `run-${this.runStartTime}-${(0, crypto_1.randomUUID)()}`;
368
333
  const runData = {
369
334
  id: runId,
@@ -374,7 +339,7 @@ class PlaywrightPulseReporter {
374
339
  skipped: 0,
375
340
  duration,
376
341
  };
377
- let finalReport;
342
+ let finalReport = undefined; // Initialize as undefined
378
343
  if (this.isSharded) {
379
344
  finalReport = await this._mergeShardResults(runData);
380
345
  }
@@ -384,42 +349,83 @@ class PlaywrightPulseReporter {
384
349
  runData.failed = this.results.filter((r) => r.status === "failed").length;
385
350
  runData.skipped = this.results.filter((r) => r.status === "skipped").length;
386
351
  runData.totalTests = this.results.length;
352
+ const reviveDates = (key, value) => {
353
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
354
+ if (typeof value === "string" && isoDateRegex.test(value)) {
355
+ const date = new Date(value);
356
+ return !isNaN(date.getTime()) ? date : value;
357
+ }
358
+ return value;
359
+ };
360
+ const properlyTypedResults = JSON.parse(JSON.stringify(this.results), reviveDates);
387
361
  finalReport = {
388
362
  run: runData,
389
- results: this.results,
363
+ results: properlyTypedResults,
390
364
  metadata: { generatedAt: new Date().toISOString() },
391
365
  };
392
366
  }
393
- // This block seems redundant as finalReport is already assigned above.
394
- // if (this.isSharded) {
395
- // finalReport = await this._mergeShardResults(runData);
396
- // } else {
397
- // this.results.forEach((r) => (r.runId = runId));
398
- // runData.passed = this.results.filter((r) => r.status === "passed").length;
399
- // runData.failed = this.results.filter((r) => r.status === "failed").length;
400
- // runData.skipped = this.results.filter((r) => r.status === "skipped").length;
401
- // runData.totalTests = this.results.length;
402
- // finalReport = {
403
- // run: runData, results: this.results,
404
- // metadata: { generatedAt: new Date().toISOString() },
405
- // };
406
- // }
407
- const finalRunStatus = ((_b = (_a = finalReport.run) === null || _a === void 0 ? void 0 : _a.failed) !== null && _b !== void 0 ? _b : 0 > 0)
367
+ if (!finalReport) {
368
+ console.error("PlaywrightPulseReporter: CRITICAL - finalReport object was not generated. Cannot create summary.");
369
+ const errorSummary = `
370
+ PlaywrightPulseReporter: Run Finished
371
+ -----------------------------------------
372
+ Overall Status: ERROR (Report data missing)
373
+ Total Tests: N/A
374
+ Passed: N/A
375
+ Failed: N/A
376
+ Skipped: N/A
377
+ Duration: N/A
378
+ -----------------------------------------`;
379
+ if (this.printsToStdio()) {
380
+ console.log(errorSummary);
381
+ }
382
+ const errorReport = {
383
+ run: {
384
+ id: runId,
385
+ timestamp: new Date(this.runStartTime),
386
+ totalTests: 0,
387
+ passed: 0,
388
+ failed: 0,
389
+ skipped: 0,
390
+ duration: duration,
391
+ },
392
+ results: [],
393
+ metadata: {
394
+ generatedAt: new Date().toISOString(),
395
+ },
396
+ };
397
+ const finalOutputPathOnError = path.join(this.outputDir, this.baseOutputFile);
398
+ try {
399
+ await this._ensureDirExists(this.outputDir);
400
+ await fs.writeFile(finalOutputPathOnError, JSON.stringify(errorReport, null, 2));
401
+ console.warn(`PlaywrightPulseReporter: Wrote an error report to ${finalOutputPathOnError} as finalReport was missing.`);
402
+ }
403
+ catch (writeError) {
404
+ console.error(`PlaywrightPulseReporter: Failed to write error report: ${writeError.message}`);
405
+ }
406
+ return;
407
+ }
408
+ const reportRunData = finalReport.run;
409
+ const finalRunStatus = ((_a = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed) !== null && _a !== void 0 ? _a : 0) > 0
408
410
  ? "failed"
409
- : ((_c = finalReport.run) === null || _c === void 0 ? void 0 : _c.totalTests) === 0
410
- ? "no tests"
411
+ : ((_b = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) !== null && _b !== void 0 ? _b : 0) === 0 && result.status !== "passed"
412
+ ? result.status === "interrupted"
413
+ ? "interrupted"
414
+ : "no tests or error"
411
415
  : "passed";
412
416
  const summary = `
413
417
  PlaywrightPulseReporter: Run Finished
414
418
  -----------------------------------------
415
419
  Overall Status: ${finalRunStatus.toUpperCase()}
416
- Total Tests: ${(_e = (_d = finalReport.run) === null || _d === void 0 ? void 0 : _d.totalTests) !== null && _e !== void 0 ? _e : "N/A"}
417
- Passed: ${(_g = (_f = finalReport.run) === null || _f === void 0 ? void 0 : _f.passed) !== null && _g !== void 0 ? _g : "N/A"}
418
- Failed: ${(_j = (_h = finalReport.run) === null || _h === void 0 ? void 0 : _h.failed) !== null && _j !== void 0 ? _j : "N/A"}
419
- Skipped: ${(_l = (_k = finalReport.run) === null || _k === void 0 ? void 0 : _k.skipped) !== null && _l !== void 0 ? _l : "N/A"}
420
- Duration: ${(duration / 1000).toFixed(2)}s
420
+ Total Tests: ${(reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.totalTests) || 0}
421
+ Passed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.passed}
422
+ Failed: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.failed}
423
+ Skipped: ${reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.skipped}
424
+ Duration: ${(((_c = reportRunData === null || reportRunData === void 0 ? void 0 : reportRunData.duration) !== null && _c !== void 0 ? _c : 0) / 1000).toFixed(2)}s
421
425
  -----------------------------------------`;
422
- console.log(summary);
426
+ if (this.printsToStdio()) {
427
+ console.log(summary);
428
+ }
423
429
  const finalOutputPath = path.join(this.outputDir, this.baseOutputFile);
424
430
  try {
425
431
  await this._ensureDirExists(this.outputDir);
@@ -430,7 +436,9 @@ PlaywrightPulseReporter: Run Finished
430
436
  return value.toString();
431
437
  return value;
432
438
  }, 2));
433
- console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
439
+ if (this.printsToStdio()) {
440
+ console.log(`PlaywrightPulseReporter: JSON report written to ${finalOutputPath}`);
441
+ }
434
442
  }
435
443
  catch (error) {
436
444
  console.error(`Pulse Reporter: Failed to write final JSON report to ${finalOutputPath}. Error: ${error.message}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arghajit/playwright-pulse-report",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "keywords": [
7
7
  "playwright",
@@ -28,7 +28,7 @@
28
28
  "generate-pulse-report": "./scripts/generate-static-report.mjs",
29
29
  "merge-pulse-report": "./scripts/merge-pulse-report.js",
30
30
  "send-email": "./scripts/sendReport.js",
31
- "generate-trend": "./scripts/generate-trend-excel.mjs"
31
+ "generate-trend": "./scripts/generate-trend.mjs"
32
32
  },
33
33
  "exports": {
34
34
  ".": {
@@ -85,6 +85,7 @@
85
85
  "dotenv": "^16.5.0",
86
86
  "firebase": "^11.3.0",
87
87
  "genkit": "^1.6.2",
88
+ "highcharts": "^12.2.0",
88
89
  "jsdom": "^26.1.0",
89
90
  "lucide-react": "^0.475.0",
90
91
  "next": "15.2.3",
@@ -97,7 +98,6 @@
97
98
  "recharts": "^2.15.1",
98
99
  "tailwind-merge": "^3.0.1",
99
100
  "tailwindcss-animate": "^1.0.7",
100
- "xlsx": "^0.18.5",
101
101
  "zod": "^3.24.2"
102
102
  },
103
103
  "devDependencies": {