@arghajit/playwright-pulse-report 0.3.0 → 0.3.2

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
@@ -90,13 +90,13 @@ All CLI scripts now support custom output directories, giving you full flexibili
90
90
 
91
91
  ```bash
92
92
  # Using custom directory
93
- npx generate-pulse-report --outputDir my-reports
93
+ npx generate-pulse-report --outputDir {YOUR_CUSTOM_REPORT_FOLDER}
94
94
  npx generate-report -o test-results/e2e
95
95
  npx send-email --outputDir custom-pulse-reports
96
96
 
97
97
  # Using nested paths
98
98
  npx generate-pulse-report --outputDir reports/integration
99
- npx merge-pulse-report --outputDir my-test-reports
99
+ npx merge-pulse-report --outputDir {YOUR_CUSTOM_REPORT_FOLDER}
100
100
  ```
101
101
 
102
102
  **Important:** Make sure your `playwright.config.ts` custom directory matches the CLI script:
@@ -105,7 +105,7 @@ npx merge-pulse-report --outputDir my-test-reports
105
105
  import { defineConfig } from "@playwright/test";
106
106
  import * as path from "path";
107
107
 
108
- const CUSTOM_REPORT_DIR = path.resolve(__dirname, "my-reports");
108
+ const CUSTOM_REPORT_DIR = path.resolve(__dirname, "{YOUR_CUSTOM_REPORT_FOLDER}");
109
109
 
110
110
  export default defineConfig({
111
111
  reporter: [
@@ -174,6 +174,67 @@ The dashboard includes AI-powered test analysis that provides:
174
174
  - Failure pattern recognition
175
175
  - Suggested optimizations
176
176
 
177
+ ## 📧 Send Report to Mail
178
+
179
+ The `send-email` CLI wraps the full email flow:
180
+
181
+ - Generates a lightweight HTML summary (`pulse-email-summary.html`) from the latest `playwright-pulse-report.json`.
182
+ - Builds a stats table (start time, duration, total, passed, failed, skipped, percentages).
183
+ - Sends an email with that summary as both the body and an HTML attachment.
184
+
185
+ ### 1. Configure Recipients
186
+
187
+ Set up to 5 recipients via environment variables:
188
+
189
+ ```bash
190
+ RECIPIENT_EMAIL_1=recipient1@example.com
191
+ RECIPIENT_EMAIL_2=recipient2@example.com
192
+ RECIPIENT_EMAIL_3=recipient3@example.com
193
+ RECIPIENT_EMAIL_4=recipient4@example.com
194
+ RECIPIENT_EMAIL_5=recipient5@example.com
195
+ ```
196
+
197
+ ### 2. Choose Credential Flow
198
+
199
+ The script supports two ways to obtain SMTP credentials:
200
+
201
+ **Flow A – Environment-based credentials (recommended)**
202
+
203
+ Provide mail host and credentials via environment variables:
204
+
205
+ ```bash
206
+ PULSE_MAIL_HOST=gmail # or: outlook
207
+ PULSE_MAIL_USERNAME=you@example.com
208
+ PULSE_MAIL_PASSWORD=your_app_password
209
+ ```
210
+
211
+ - `PULSE_MAIL_HOST` supports `gmail` or `outlook` only.
212
+ - For Gmail/Outlook, use an app password or SMTP-enabled credentials.
213
+
214
+ **Flow B – Default Flow (fallback)**
215
+
216
+ If the above variables are not set, the script fallbacks to default the mail host for compatibility.
217
+
218
+ ### 3. Run the CLI
219
+
220
+ Use the default output directory:
221
+
222
+ ```bash
223
+ npx send-email
224
+ ```
225
+
226
+ Or point to a custom report directory (must contain `playwright-pulse-report.json`):
227
+
228
+ ```bash
229
+ npx send-email --outputDir <YOUR_CUSTOM_REPORT_FOLDER>
230
+ ```
231
+
232
+ Under the hood, this will:
233
+
234
+ - Resolve the report directory (from `--outputDir` or `playwright.config.ts`).
235
+ - Run `generate-email-report.mjs` to create `pulse-email-summary.html`.
236
+ - Use Nodemailer to send the email via the selected provider (Gmail or Outlook).
237
+
177
238
  ## ⚙️ CI/CD Integration
178
239
 
179
240
  ### Basic Workflow
@@ -0,0 +1,12 @@
1
+ export type PulseSeverityLevel = "Minor" | "Low" | "Medium" | "High" | "Critical";
2
+ export declare const pulse: {
3
+ /**
4
+ * Sets the severity level for the current test.
5
+ * * @param level - The severity level ('Minor' | 'Low' | 'Medium' | 'High' | 'Critical')
6
+ * @example
7
+ * test('Login', async () => {
8
+ * pulse.severity('Critical');
9
+ * });
10
+ */
11
+ severity: (level: PulseSeverityLevel) => void;
12
+ };
package/dist/pulse.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pulse = void 0;
4
+ const test_1 = require("@playwright/test");
5
+ exports.pulse = {
6
+ /**
7
+ * Sets the severity level for the current test.
8
+ * * @param level - The severity level ('Minor' | 'Low' | 'Medium' | 'High' | 'Critical')
9
+ * @example
10
+ * test('Login', async () => {
11
+ * pulse.severity('Critical');
12
+ * });
13
+ */
14
+ severity: (level) => {
15
+ const validLevels = ["Minor", "Low", "Medium", "High", "Critical"];
16
+ // Default to "Medium" if an invalid string is passed
17
+ const selectedLevel = validLevels.includes(level) ? level : "Medium";
18
+ // Add the annotation to Playwright's test info
19
+ test_1.test.info().annotations.push({
20
+ type: "pulse_severity",
21
+ description: selectedLevel,
22
+ });
23
+ },
24
+ };
@@ -3,3 +3,5 @@ export default PlaywrightPulseReporter;
3
3
  export { PlaywrightPulseReporter };
4
4
  export type { PlaywrightPulseReport } from "../lib/report-types";
5
5
  export type { TestResult, TestRun, TestStep, TestStatus } from "../types";
6
+ export { pulse } from "../pulse";
7
+ export type { PulseSeverityLevel } from "../pulse";
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PlaywrightPulseReporter = void 0;
3
+ exports.pulse = exports.PlaywrightPulseReporter = void 0;
4
4
  // src/reporter/index.ts
5
5
  const playwright_pulse_reporter_1 = require("./playwright-pulse-reporter");
6
6
  Object.defineProperty(exports, "PlaywrightPulseReporter", { enumerable: true, get: function () { return playwright_pulse_reporter_1.PlaywrightPulseReporter; } });
7
7
  // Export the reporter class as the default export for CommonJS compatibility
8
8
  // and also as a named export for potential ES module consumers.
9
9
  exports.default = playwright_pulse_reporter_1.PlaywrightPulseReporter;
10
+ // --- NEW: Export the pulse helper ---
11
+ // This allows: import { pulse } from '@arghajit/playwright-pulse-report';
12
+ var pulse_1 = require("../pulse"); // Adjust path based on where you placed pulse.ts
13
+ Object.defineProperty(exports, "pulse", { enumerable: true, get: function () { return pulse_1.pulse; } });
@@ -16,6 +16,7 @@ export declare class PlaywrightPulseReporter implements Reporter {
16
16
  printsToStdio(): boolean;
17
17
  onBegin(config: FullConfig, suite: Suite): void;
18
18
  onTestBegin(test: TestCase): void;
19
+ private _getSeverity;
19
20
  private getBrowserDetails;
20
21
  private processStep;
21
22
  onTestEnd(test: TestCase, result: PwTestResult): Promise<void>;
@@ -32,12 +32,15 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.PlaywrightPulseReporter = void 0;
37
40
  const fs = __importStar(require("fs/promises"));
38
41
  const path = __importStar(require("path"));
39
42
  const crypto_1 = require("crypto");
40
- const ua_parser_js_1 = require("ua-parser-js");
43
+ const ua_parser_js_1 = __importDefault(require("ua-parser-js"));
41
44
  const os = __importStar(require("os"));
42
45
  const convertStatus = (status, testCase) => {
43
46
  if ((testCase === null || testCase === void 0 ? void 0 : testCase.expectedStatus) === "failed") {
@@ -109,13 +112,17 @@ class PlaywrightPulseReporter {
109
112
  onTestBegin(test) {
110
113
  console.log(`Starting test: ${test.title}`);
111
114
  }
115
+ _getSeverity(annotations) {
116
+ const severityAnnotation = annotations.find((a) => a.type === "pulse_severity");
117
+ return (severityAnnotation === null || severityAnnotation === void 0 ? void 0 : severityAnnotation.description) || "Medium";
118
+ }
112
119
  getBrowserDetails(test) {
113
120
  var _a, _b, _c, _d;
114
121
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
115
122
  const projectConfig = project === null || project === void 0 ? void 0 : project.use;
116
123
  const userAgent = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.userAgent;
117
124
  const configuredBrowserType = (_b = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
118
- const parser = new ua_parser_js_1.UAParser(userAgent);
125
+ const parser = new ua_parser_js_1.default(userAgent);
119
126
  const result = parser.getResult();
120
127
  let browserName = result.browser.name;
121
128
  const browserVersion = result.browser.version
@@ -197,7 +204,7 @@ class PlaywrightPulseReporter {
197
204
  };
198
205
  }
199
206
  async onTestEnd(test, result) {
200
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
207
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
201
208
  const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
202
209
  const browserDetails = this.getBrowserDetails(test);
203
210
  const testStatus = convertStatus(result.status, test);
@@ -224,6 +231,16 @@ class PlaywrightPulseReporter {
224
231
  catch (e) {
225
232
  console.warn(`Pulse Reporter: Could not extract code snippet for ${test.title}`, e);
226
233
  }
234
+ // 1. Get Spec File Name
235
+ const specFileName = ((_e = test.location) === null || _e === void 0 ? void 0 : _e.file)
236
+ ? path.basename(test.location.file)
237
+ : "n/a";
238
+ // 2. Get Describe Block Name
239
+ // Check if the immediate parent is a 'describe' block
240
+ let describeBlockName = "n/a";
241
+ if (((_f = test.parent) === null || _f === void 0 ? void 0 : _f.type) === "describe") {
242
+ describeBlockName = test.parent.title;
243
+ }
227
244
  const stdoutMessages = result.stdout.map((item) => typeof item === "string" ? item : item.toString());
228
245
  const stderrMessages = result.stderr.map((item) => typeof item === "string" ? item : item.toString());
229
246
  const maxWorkers = this.config.workers;
@@ -241,27 +258,30 @@ class PlaywrightPulseReporter {
241
258
  const pulseResult = {
242
259
  id: test.id,
243
260
  runId: "TBD",
261
+ describe: describeBlockName,
262
+ spec_file: specFileName,
244
263
  name: test.titlePath().join(" > "),
245
- suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_e = this.config.projects[0]) === null || _e === void 0 ? void 0 : _e.name) || "Default Suite",
264
+ suiteName: (project === null || project === void 0 ? void 0 : project.name) || ((_g = this.config.projects[0]) === null || _g === void 0 ? void 0 : _g.name) || "Default Suite",
246
265
  status: testStatus,
247
266
  duration: result.duration,
248
267
  startTime: startTime,
249
268
  endTime: endTime,
250
269
  browser: browserDetails,
251
270
  retries: result.retry,
252
- steps: ((_f = result.steps) === null || _f === void 0 ? void 0 : _f.length) ? await processAllSteps(result.steps) : [],
253
- errorMessage: (_g = result.error) === null || _g === void 0 ? void 0 : _g.message,
254
- stackTrace: (_h = result.error) === null || _h === void 0 ? void 0 : _h.stack,
255
- snippet: (_j = result.error) === null || _j === void 0 ? void 0 : _j.snippet,
271
+ steps: ((_h = result.steps) === null || _h === void 0 ? void 0 : _h.length) ? await processAllSteps(result.steps) : [],
272
+ errorMessage: (_j = result.error) === null || _j === void 0 ? void 0 : _j.message,
273
+ stackTrace: (_k = result.error) === null || _k === void 0 ? void 0 : _k.stack,
274
+ snippet: (_l = result.error) === null || _l === void 0 ? void 0 : _l.snippet,
256
275
  codeSnippet: codeSnippet,
257
276
  tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
277
+ severity: this._getSeverity(test.annotations),
258
278
  screenshots: [],
259
279
  videoPath: [],
260
280
  tracePath: undefined,
261
281
  attachments: [],
262
282
  stdout: stdoutMessages.length > 0 ? stdoutMessages : undefined,
263
283
  stderr: stderrMessages.length > 0 ? stderrMessages : undefined,
264
- annotations: ((_k = test.annotations) === null || _k === void 0 ? void 0 : _k.length) > 0 ? test.annotations : undefined,
284
+ annotations: ((_m = test.annotations) === null || _m === void 0 ? void 0 : _m.length) > 0 ? test.annotations : undefined,
265
285
  ...testSpecificData,
266
286
  };
267
287
  for (const [index, attachment] of result.attachments.entries()) {
@@ -278,16 +298,16 @@ class PlaywrightPulseReporter {
278
298
  await this._ensureDirExists(path.dirname(absoluteDestPath));
279
299
  await fs.copyFile(attachment.path, absoluteDestPath);
280
300
  if (attachment.contentType.startsWith("image/")) {
281
- (_l = pulseResult.screenshots) === null || _l === void 0 ? void 0 : _l.push(relativeDestPath);
301
+ (_o = pulseResult.screenshots) === null || _o === void 0 ? void 0 : _o.push(relativeDestPath);
282
302
  }
283
303
  else if (attachment.contentType.startsWith("video/")) {
284
- (_m = pulseResult.videoPath) === null || _m === void 0 ? void 0 : _m.push(relativeDestPath);
304
+ (_p = pulseResult.videoPath) === null || _p === void 0 ? void 0 : _p.push(relativeDestPath);
285
305
  }
286
306
  else if (attachment.name === "trace") {
287
307
  pulseResult.tracePath = relativeDestPath;
288
308
  }
289
309
  else {
290
- (_o = pulseResult.attachments) === null || _o === void 0 ? void 0 : _o.push({
310
+ (_q = pulseResult.attachments) === null || _q === void 0 ? void 0 : _q.push({
291
311
  name: attachment.name,
292
312
  path: relativeDestPath,
293
313
  contentType: attachment.contentType,
@@ -17,6 +17,8 @@ export interface TestStep {
17
17
  }
18
18
  export interface TestResult {
19
19
  id: string;
20
+ describe?: string;
21
+ spec_file?: string;
20
22
  name: string;
21
23
  status: TestStatus;
22
24
  duration: number;
@@ -29,6 +31,7 @@ export interface TestResult {
29
31
  snippet?: string;
30
32
  codeSnippet?: string;
31
33
  tags?: string[];
34
+ severity?: "Minor" | "Low" | "Medium" | "High" | "Critical";
32
35
  suiteName?: string;
33
36
  runId: string;
34
37
  browser: string;
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@arghajit/playwright-pulse-report",
3
3
  "author": "Arghajit Singha",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "homepage": "https://playwright-pulse-report.netlify.app/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Arghajit47/playwright-pulse"
10
+ },
7
11
  "keywords": [
8
12
  "playwright",
9
13
  "reporter",
@@ -30,12 +34,12 @@
30
34
  ],
31
35
  "license": "MIT",
32
36
  "bin": {
33
- "generate-pulse-report": "./scripts/generate-static-report.mjs",
34
- "generate-report": "./scripts/generate-report.mjs",
35
- "merge-pulse-report": "./scripts/merge-pulse-report.js",
36
- "send-email": "./scripts/sendReport.mjs",
37
- "generate-trend": "./scripts/generate-trend.mjs",
38
- "generate-email-report": "./scripts/generate-email-report.mjs"
37
+ "generate-pulse-report": "scripts/generate-static-report.mjs",
38
+ "generate-report": "scripts/generate-report.mjs",
39
+ "merge-pulse-report": "scripts/merge-pulse-report.js",
40
+ "send-email": "scripts/sendReport.mjs",
41
+ "generate-trend": "scripts/generate-trend.mjs",
42
+ "generate-email-report": "scripts/generate-email-report.mjs"
39
43
  },
40
44
  "exports": {
41
45
  ".": {
@@ -68,7 +72,7 @@
68
72
  "nodemailer": "^7.0.3",
69
73
  "patch-package": "^8.0.0",
70
74
  "recharts": "^2.15.1",
71
- "ua-parser-js": "^2.0.3",
75
+ "ua-parser-js": "^1.0.41",
72
76
  "zod": "^3.24.2"
73
77
  },
74
78
  "devDependencies": {
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import * as fs from "fs";
3
2
  import * as path from "path";
4
3
  import { pathToFileURL } from "url";
@@ -24,9 +23,61 @@ async function findPlaywrightConfig() {
24
23
  }
25
24
 
26
25
  async function extractOutputDirFromConfig(configPath) {
26
+ let fileContent = "";
27
27
  try {
28
- let config;
28
+ fileContent = fs.readFileSync(configPath, "utf-8");
29
+ } catch (e) {
30
+ // If we can't read the file, we can't parse or import it.
31
+ return null;
32
+ }
33
+
34
+ // 1. Strategy: Text Parsing (Safe & Fast)
35
+ // We try to read the file as text first. This finds the outputDir without
36
+ // triggering any Node.js warnings or errors.
37
+ try {
38
+ // Regex matches: outputDir: "value" or outputDir: 'value'
39
+ const match = fileContent.match(/outputDir:\s*["']([^"']+)["']/);
40
+
41
+ if (match && match[1]) {
42
+ return path.resolve(process.cwd(), match[1]);
43
+ }
44
+ } catch (e) {
45
+ // Ignore text reading errors
46
+ }
29
47
 
48
+ // 2. Safety Check: Detect ESM in CJS to Prevent Node Warnings
49
+ // The warning "To load an ES module..." happens when we try to import()
50
+ // a .js file containing ESM syntax (import/export) in a CJS package.
51
+ // We explicitly check for this and ABORT the import if found.
52
+ if (configPath.endsWith(".js")) {
53
+ let isModulePackage = false;
54
+ try {
55
+ const pkgPath = path.resolve(process.cwd(), "package.json");
56
+ if (fs.existsSync(pkgPath)) {
57
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
58
+ isModulePackage = pkg.type === "module";
59
+ }
60
+ } catch (e) {}
61
+
62
+ if (!isModulePackage) {
63
+ // Heuristic: Check for ESM syntax (import/export at start of lines)
64
+ const hasEsmSyntax =
65
+ /^\s*import\s+/m.test(fileContent) ||
66
+ /^\s*export\s+/m.test(fileContent);
67
+
68
+ if (hasEsmSyntax) {
69
+ // We found ESM syntax in a .js file within a CJS project.
70
+ // Attempting to import this WILL trigger the Node.js warning.
71
+ // Since regex failed to find outputDir, and we can't import safely, we abort now.
72
+ return null;
73
+ }
74
+ }
75
+ }
76
+
77
+ // 3. Strategy: Dynamic Import
78
+ // If we passed the safety check, we try to import the config.
79
+ try {
80
+ let config;
30
81
  const configDir = dirname(configPath);
31
82
  const originalDirname = global.__dirname;
32
83
  const originalFilename = global.__filename;
@@ -39,73 +90,70 @@ async function extractOutputDirFromConfig(configPath) {
39
90
  try {
40
91
  const { register } = await import("node:module");
41
92
  const { pathToFileURL } = await import("node:url");
42
-
43
93
  register("ts-node/esm", pathToFileURL("./"));
44
-
45
94
  config = await import(pathToFileURL(configPath).href);
46
95
  } catch (tsError) {
47
- try {
48
- const tsNode = await import("ts-node");
49
- tsNode.register({
50
- transpileOnly: true,
51
- compilerOptions: {
52
- module: "ESNext",
53
- },
54
- });
55
- config = await import(pathToFileURL(configPath).href);
56
- } catch (fallbackError) {
57
- console.error("Failed to load TypeScript config:", fallbackError);
58
- return null;
59
- }
96
+ const tsNode = await import("ts-node");
97
+ tsNode.register({
98
+ transpileOnly: true,
99
+ compilerOptions: { module: "commonjs" },
100
+ });
101
+ config = require(configPath);
60
102
  }
61
103
  } else {
104
+ // Try dynamic import for JS/MJS
62
105
  config = await import(pathToFileURL(configPath).href);
63
106
  }
64
- } finally {
65
- if (originalDirname !== undefined) {
66
- global.__dirname = originalDirname;
67
- } else {
68
- delete global.__dirname;
69
- }
70
- if (originalFilename !== undefined) {
71
- global.__filename = originalFilename;
72
- } else {
73
- delete global.__filename;
107
+
108
+ // Handle Default Export
109
+ if (config && config.default) {
110
+ config = config.default;
74
111
  }
75
- }
76
112
 
77
- const playwrightConfig = config.default || config;
78
-
79
- if (playwrightConfig && Array.isArray(playwrightConfig.reporter)) {
80
- for (const reporterConfig of playwrightConfig.reporter) {
81
- if (Array.isArray(reporterConfig)) {
82
- const [reporterPath, options] = reporterConfig;
83
-
84
- if (
85
- typeof reporterPath === "string" &&
86
- (reporterPath.includes("playwright-pulse-report") ||
87
- reporterPath.includes("@arghajit/playwright-pulse-report") ||
88
- reporterPath.includes("@arghajit/dummy"))
89
- ) {
90
- if (options && options.outputDir) {
91
- const resolvedPath =
92
- typeof options.outputDir === "string"
93
- ? options.outputDir
94
- : options.outputDir;
95
- console.log(`Found outputDir in config: ${resolvedPath}`);
96
- return path.resolve(process.cwd(), resolvedPath);
113
+ if (config) {
114
+ // Check for Reporter Config
115
+ if (config.reporter) {
116
+ const reporters = Array.isArray(config.reporter)
117
+ ? config.reporter
118
+ : [config.reporter];
119
+
120
+ for (const reporter of reporters) {
121
+ const reporterName = Array.isArray(reporter)
122
+ ? reporter[0]
123
+ : reporter;
124
+ const reporterOptions = Array.isArray(reporter)
125
+ ? reporter[1]
126
+ : null;
127
+
128
+ if (
129
+ typeof reporterName === "string" &&
130
+ (reporterName.includes("playwright-pulse-report") ||
131
+ reporterName.includes("@arghajit/playwright-pulse-report") ||
132
+ reporterName.includes("@arghajit/dummy"))
133
+ ) {
134
+ if (reporterOptions && reporterOptions.outputDir) {
135
+ return path.resolve(process.cwd(), reporterOptions.outputDir);
136
+ }
97
137
  }
98
138
  }
99
139
  }
140
+
141
+ // Check for Global outputDir
142
+ if (config.outputDir) {
143
+ return path.resolve(process.cwd(), config.outputDir);
144
+ }
100
145
  }
146
+ } finally {
147
+ // Clean up globals
148
+ global.__dirname = originalDirname;
149
+ global.__filename = originalFilename;
101
150
  }
102
-
103
- console.log("No matching reporter config found with outputDir");
104
- return null;
105
151
  } catch (error) {
106
- console.error("Error extracting outputDir from config:", error);
152
+ // SILENT CATCH: Do NOT log anything here.
107
153
  return null;
108
154
  }
155
+
156
+ return null;
109
157
  }
110
158
 
111
159
  export async function getOutputDir(customOutputDirFromArgs = null) {
@@ -217,6 +217,40 @@ function generateMinifiedHTML(reportData) {
217
217
  const testFileParts = test.name.split(" > ");
218
218
  const testTitle =
219
219
  testFileParts[testFileParts.length - 1] || "Unnamed Test";
220
+
221
+ // --- NEW: Severity Logic ---
222
+ const severity = test.severity || "Medium";
223
+ const getSeverityColor = (level) => {
224
+ switch (level) {
225
+ case "Minor":
226
+ return "#006064";
227
+ case "Low":
228
+ return "#FFA07A";
229
+ case "Medium":
230
+ return "#577A11";
231
+ case "High":
232
+ return "#B71C1C";
233
+ case "Critical":
234
+ return "#64158A";
235
+ default:
236
+ return "#577A11";
237
+ }
238
+ };
239
+ // We use inline styles here to ensure they render correctly in emails
240
+ const severityBadge = `<span style="background-color: ${getSeverityColor(
241
+ severity
242
+ )}; font-size: 0.8em; font-weight: 600; padding: 3px 8px; border-radius: 4px; color: #fff; margin-left: 10px; white-space: nowrap;">${severity}</span>`;
243
+
244
+ // --- NEW: Tags Logic ---
245
+ const tagsBadges = (test.tags || [])
246
+ .map(
247
+ (tag) =>
248
+ `<span style="background-color: #7f8c8d; font-size: 0.8em; font-weight: 600; padding: 3px 8px; border-radius: 4px; color: #fff; margin-left: 5px; white-space: nowrap;">${sanitizeHTML(
249
+ tag
250
+ )}</span>`
251
+ )
252
+ .join("");
253
+
220
254
  html += `
221
255
  <li class="test-item ${getStatusClass(test.status)}"
222
256
  data-test-name-min="${sanitizeHTML(testTitle.toLowerCase())}"
@@ -230,9 +264,9 @@ function generateMinifiedHTML(reportData) {
230
264
  <span class="test-title-text" title="${sanitizeHTML(
231
265
  test.name
232
266
  )}">${sanitizeHTML(testTitle)}</span>
233
- <span class="test-status-label">${String(
234
- test.status
235
- ).toUpperCase()}</span>
267
+
268
+ ${severityBadge}
269
+ ${tagsBadges}
236
270
  </li>
237
271
  `;
238
272
  });
@@ -250,9 +284,9 @@ function generateMinifiedHTML(reportData) {
250
284
  <head>
251
285
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
252
286
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
253
- <link rel="icon" type="image/png" href="https://i.postimg.cc/v817w4sg/logo.png">
254
- <link rel="apple-touch-icon" href="https://i.postimg.cc/v817w4sg/logo.png">
255
- <title>Playwright Pulse Summary Report</title>
287
+ <link rel="icon" type="image/png" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
288
+ <link rel="apple-touch-icon" href="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png">
289
+ <title>Pulse Summary Report</title>
256
290
  <style>
257
291
  :root {
258
292
  --primary-color: #2c3e50; /* Dark Blue/Grey */
@@ -492,8 +526,8 @@ function generateMinifiedHTML(reportData) {
492
526
  <div class="container">
493
527
  <header class="report-header">
494
528
  <div class="report-header-title">
495
- <img id="report-logo" src="https://i.postimg.cc/v817w4sg/logo.png" alt="Report Logo">
496
- <h1>Playwright Pulse Summary</h1>
529
+ <img id="report-logo" src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Report Logo">
530
+ <h1>Pulse Summary Report</h1>
497
531
  </div>
498
532
  <div class="run-info">
499
533
  <strong>Run Date:</strong> ${formatDate(
@@ -527,8 +561,24 @@ function generateMinifiedHTML(reportData) {
527
561
  </section>
528
562
 
529
563
  <section class="test-results-section">
530
- <h1 class="section-title">Test Case Summary</h1>
531
-
564
+ <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 30px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid var(--secondary-color);">
565
+ <h1 style="margin: 0; font-size: 1.5em; color: var(--primary-color);">Test Case Summary</h1>
566
+ <div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 0.75em;">
567
+ <span style="font-weight: 600; color: var(--dark-gray-color);">Legend:</span>
568
+
569
+ <span style="margin-left: 4px; font-weight: 600; color: var(--text-color);">Severity:</span>
570
+
571
+ <span style="background-color: #006064; color: #fff; padding: 2px 6px; border-radius: 3px;">Minor</span>
572
+ <span style="background-color: #FFA07A; color: #fff; padding: 2px 6px; border-radius: 3px;">Low</span>
573
+ <span style="background-color: #577A11; color: #fff; padding: 2px 6px; border-radius: 3px;">Medium</span>
574
+ <span style="background-color: #B71C1C; color: #fff; padding: 2px 6px; border-radius: 3px;">High</span>
575
+ <span style="background-color: #64158A; color: #fff; padding: 2px 6px; border-radius: 3px;">Critical</span>
576
+
577
+ <span style="border-left: 1px solid #ccc; height: 14px; margin: 0 4px;"></span>
578
+
579
+ <span style="background-color: #7f8c8d; color: #fff; padding: 2px 6px; border-radius: 3px;">Tags</span>
580
+ </div>
581
+ </div>
532
582
  <div class="filters-section">
533
583
  <input type="text" id="filter-min-name" placeholder="Search by test name...">
534
584
  <select id="filter-min-status">