@arghajit/playwright-pulse-report 0.1.1 → 0.1.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
@@ -207,7 +207,7 @@ To work on the reporter or the dashboard itself:
207
207
 
208
208
  * This project supports Playwright test execution with Pulse Reporting in GitHub Actions. Here's how Pulse reports are managed:
209
209
 
210
- ```
210
+ ```bash
211
211
  # Upload Pulse report from each shard (per matrix.config.type)
212
212
  - name: Upload Pulse Report results
213
213
  if: success() || failure()
@@ -241,7 +241,7 @@ To work on the reporter or the dashboard itself:
241
241
 
242
242
  * This project supports sharded Playwright test execution with Pulse Reporting in GitHub Actions. Here's how Pulse reports are managed across shards:
243
243
 
244
- ```
244
+ ```bash
245
245
  # Upload Pulse report from each shard (per matrix.config.type)
246
246
  - name: Upload Pulse Report results
247
247
  if: success() || failure()
@@ -280,6 +280,7 @@ To work on the reporter or the dashboard itself:
280
280
  name: pulse-report
281
281
  path: pulse-report/
282
282
  ```
283
+
283
284
  ## 🧠 Notes:
284
285
 
285
286
  * Each shard generates its own playwright-pulse-report.json inside pulse-report/.
@@ -287,3 +288,8 @@ To work on the reporter or the dashboard itself:
287
288
  * After the test matrix completes, reports are downloaded, renamed, and merged.
288
289
  * merge-report is a custom Node.js script that combines all JSON files into one.
289
290
  * generate-report can build a static HTML dashboard if needed.
291
+
292
+ ## Fixes:
293
+
294
+ ### - "0.1.1" : Added Sharding Support
295
+ ### - "0.1.2" : Fixed browser filter and Added Browser Tag in Test Suite Card
@@ -45,100 +45,62 @@ const ATTACHMENTS_SUBDIR = "attachments"; // Consistent subdirectory name
45
45
  * @param config The reporter configuration options.
46
46
  */
47
47
  function attachFiles(testId, pwResult, pulseResult, config) {
48
- const baseReportDir = config.outputDir || "pulse-report"; // Base output directory
49
- // Ensure attachments are relative to the main outputDir
50
- const attachmentsBaseDir = path.resolve(baseReportDir, ATTACHMENTS_SUBDIR); // Absolute path for FS operations
51
- const attachmentsSubFolder = testId.replace(/[^a-zA-Z0-9_-]/g, "_"); // Sanitize testId for folder name
52
- const testAttachmentsDir = path.join(
53
- attachmentsBaseDir,
54
- attachmentsSubFolder
55
- ); // e.g., pulse-report/attachments/test_id_abc
56
- try {
57
- if (!fs.existsSync(testAttachmentsDir)) {
58
- fs.mkdirSync(testAttachmentsDir, { recursive: true });
59
- }
60
- } catch (error) {
61
- console.error(
62
- `Pulse Reporter: Failed to create attachments directory: ${testAttachmentsDir}`,
63
- error
64
- );
65
- return; // Stop processing if directory creation fails
66
- }
67
- if (!pwResult.attachments) return;
68
- const { base64Images } = config; // Get base64 embedding option
69
- pulseResult.screenshots = []; // Initialize screenshots array
70
- pwResult.attachments.forEach((attachment) => {
71
- const { contentType, name, path: attachmentPath, body } = attachment;
72
- // Skip attachments without path or body
73
- if (!attachmentPath && !body) {
74
- console.warn(
75
- `Pulse Reporter: Attachment "${name}" for test ${testId} has no path or body. Skipping.`
76
- );
77
- return;
48
+ const baseReportDir = config.outputDir || "pulse-report"; // Base output directory
49
+ // Ensure attachments are relative to the main outputDir
50
+ const attachmentsBaseDir = path.resolve(baseReportDir, ATTACHMENTS_SUBDIR); // Absolute path for FS operations
51
+ const attachmentsSubFolder = testId.replace(/[^a-zA-Z0-9_-]/g, "_"); // Sanitize testId for folder name
52
+ const testAttachmentsDir = path.join(attachmentsBaseDir, attachmentsSubFolder); // e.g., pulse-report/attachments/test_id_abc
53
+ try {
54
+ if (!fs.existsSync(testAttachmentsDir)) {
55
+ fs.mkdirSync(testAttachmentsDir, { recursive: true });
56
+ }
78
57
  }
79
- // Determine filename
80
- const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, "_"); // Sanitize original name
81
- const extension = attachmentPath
82
- ? path.extname(attachmentPath)
83
- : `.${getFileExtension(contentType)}`;
84
- const baseFilename = attachmentPath
85
- ? path.basename(attachmentPath, extension)
86
- : safeName;
87
- // Ensure unique filename within the test's attachment folder
88
- const fileName = `${baseFilename}_${Date.now()}${extension}`;
89
- // Relative path for storing in JSON (relative to baseReportDir)
90
- const relativePath = path.join(
91
- ATTACHMENTS_SUBDIR,
92
- attachmentsSubFolder,
93
- fileName
94
- );
95
- // Full path for file system operations
96
- const fullPath = path.join(testAttachmentsDir, fileName);
97
- if (
98
- contentType === null || contentType === void 0
99
- ? void 0
100
- : contentType.startsWith("image/")
101
- ) {
102
- // Handle all image types consistently
103
- handleImage(
104
- attachmentPath,
105
- body,
106
- base64Images,
107
- fullPath,
108
- relativePath,
109
- pulseResult,
110
- name
111
- );
112
- } else if (
113
- name === "video" ||
114
- (contentType === null || contentType === void 0
115
- ? void 0
116
- : contentType.startsWith("video/"))
117
- ) {
118
- handleAttachment(
119
- attachmentPath,
120
- body,
121
- fullPath,
122
- relativePath,
123
- "videoPath",
124
- pulseResult
125
- );
126
- } else if (name === "trace" || contentType === "application/zip") {
127
- // Trace files are zips
128
- handleAttachment(
129
- attachmentPath,
130
- body,
131
- fullPath,
132
- relativePath,
133
- "tracePath",
134
- pulseResult
135
- );
136
- } else {
137
- // Handle other generic attachments if needed (e.g., log files)
138
- // console.log(`Pulse Reporter: Processing generic attachment "${name}" (Type: ${contentType}) for test ${testId}`);
139
- // handleAttachment(attachmentPath, body, fullPath, relativePath, 'otherAttachments', pulseResult); // Example for storing other types
58
+ catch (error) {
59
+ console.error(`Pulse Reporter: Failed to create attachments directory: ${testAttachmentsDir}`, error);
60
+ return; // Stop processing if directory creation fails
140
61
  }
141
- });
62
+ if (!pwResult.attachments)
63
+ return;
64
+ const { base64Images } = config; // Get base64 embedding option
65
+ pulseResult.screenshots = []; // Initialize screenshots array
66
+ pwResult.attachments.forEach((attachment) => {
67
+ const { contentType, name, path: attachmentPath, body } = attachment;
68
+ // Skip attachments without path or body
69
+ if (!attachmentPath && !body) {
70
+ console.warn(`Pulse Reporter: Attachment "${name}" for test ${testId} has no path or body. Skipping.`);
71
+ return;
72
+ }
73
+ // Determine filename
74
+ const safeName = name.replace(/[^a-zA-Z0-9_.-]/g, "_"); // Sanitize original name
75
+ const extension = attachmentPath
76
+ ? path.extname(attachmentPath)
77
+ : `.${getFileExtension(contentType)}`;
78
+ const baseFilename = attachmentPath
79
+ ? path.basename(attachmentPath, extension)
80
+ : safeName;
81
+ // Ensure unique filename within the test's attachment folder
82
+ const fileName = `${baseFilename}_${Date.now()}${extension}`;
83
+ // Relative path for storing in JSON (relative to baseReportDir)
84
+ const relativePath = path.join(ATTACHMENTS_SUBDIR, attachmentsSubFolder, fileName);
85
+ // Full path for file system operations
86
+ const fullPath = path.join(testAttachmentsDir, fileName);
87
+ if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("image/")) {
88
+ // Handle all image types consistently
89
+ handleImage(attachmentPath, body, base64Images, fullPath, relativePath, pulseResult, name);
90
+ }
91
+ else if (name === "video" || (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("video/"))) {
92
+ handleAttachment(attachmentPath, body, fullPath, relativePath, "videoPath", pulseResult);
93
+ }
94
+ else if (name === "trace" || contentType === "application/zip") {
95
+ // Trace files are zips
96
+ handleAttachment(attachmentPath, body, fullPath, relativePath, "tracePath", pulseResult);
97
+ }
98
+ else {
99
+ // Handle other generic attachments if needed (e.g., log files)
100
+ // console.log(`Pulse Reporter: Processing generic attachment "${name}" (Type: ${contentType}) for test ${testId}`);
101
+ // handleAttachment(attachmentPath, body, fullPath, relativePath, 'otherAttachments', pulseResult); // Example for storing other types
102
+ }
103
+ });
142
104
  }
143
105
  /**
144
106
  * Handles image attachments, either embedding as base64 or copying the file.
@@ -72,10 +72,7 @@ class PlaywrightPulseReporter {
72
72
  this.baseOutputFile = (_a = options.outputFile) !== null && _a !== void 0 ? _a : this.baseOutputFile;
73
73
  // Determine outputDir relative to config file or rootDir
74
74
  // The actual resolution happens in onBegin where config is available
75
- this.outputDir =
76
- (_b = options.outputDir) !== null && _b !== void 0
77
- ? _b
78
- : "pulse-report";
75
+ this.outputDir = (_b = options.outputDir) !== null && _b !== void 0 ? _b : "pulse-report";
79
76
  this.attachmentsDir = path.join(this.outputDir, ATTACHMENTS_SUBDIR); // Initial path, resolved fully in onBegin
80
77
  // console.log(`Pulse Reporter Init: Configured outputDir option: ${options.outputDir}, Base file: ${this.baseOutputFile}`);
81
78
  }
@@ -93,12 +90,7 @@ class PlaywrightPulseReporter {
93
90
  const configFileDir = this.config.configFile
94
91
  ? path.dirname(this.config.configFile)
95
92
  : configDir;
96
- this.outputDir = path.resolve(
97
- configFileDir,
98
- (_a = this.options.outputDir) !== null && _a !== void 0
99
- ? _a
100
- : "pulse-report"
101
- );
93
+ this.outputDir = path.resolve(configFileDir, (_a = this.options.outputDir) !== null && _a !== void 0 ? _a : "pulse-report");
102
94
  // Resolve attachmentsDir relative to the final outputDir
103
95
  this.attachmentsDir = path.resolve(this.outputDir, ATTACHMENTS_SUBDIR);
104
96
  // Update options with the resolved absolute path for internal use
@@ -129,7 +121,7 @@ class PlaywrightPulseReporter {
129
121
  // Optional: Log test start if needed
130
122
  // console.log(`Starting test: ${test.title}`);
131
123
  }
132
- async processStep(step, testId) {
124
+ async processStep(step, testId, browserName) {
133
125
  var _a, _b, _c, _d;
134
126
  // Determine actual step status (don't inherit from parent)
135
127
  let stepStatus = "passed";
@@ -156,6 +148,7 @@ class PlaywrightPulseReporter {
156
148
  duration: duration,
157
149
  startTime: startTime,
158
150
  endTime: endTime,
151
+ browser: browserName,
159
152
  errorMessage: errorMessage,
160
153
  stackTrace: ((_d = step.error) === null || _d === void 0 ? void 0 : _d.stack) || undefined,
161
154
  codeLocation: codeLocation || undefined,
@@ -169,7 +162,10 @@ class PlaywrightPulseReporter {
169
162
  };
170
163
  }
171
164
  async onTestEnd(test, result) {
172
- var _a, _b, _c, _d, _e, _f, _g;
165
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
166
+ // Get the most accurate browser name
167
+ const project = (_a = test.parent) === null || _a === void 0 ? void 0 : _a.project();
168
+ const browserName = ((_b = project === null || project === void 0 ? void 0 : project.use) === null || _b === void 0 ? void 0 : _b.defaultBrowserType) || "unknown";
173
169
  const testStatus = convertStatus(result.status, test);
174
170
  const startTime = new Date(result.startTime);
175
171
  const endTime = new Date(startTime.getTime() + result.duration);
@@ -183,7 +179,7 @@ class PlaywrightPulseReporter {
183
179
  const processAllSteps = async (steps, parentTestStatus) => {
184
180
  let processed = [];
185
181
  for (const step of steps) {
186
- const processedStep = await this.processStep(step, testIdForFiles);
182
+ const processedStep = await this.processStep(step, testIdForFiles, browserName);
187
183
  processed.push(processedStep);
188
184
  if (step.steps && step.steps.length > 0) {
189
185
  const nestedSteps = await processAllSteps(step.steps, processedStep.status);
@@ -196,7 +192,7 @@ class PlaywrightPulseReporter {
196
192
  // --- Extract Code Snippet ---
197
193
  let codeSnippet = undefined;
198
194
  try {
199
- if (((_a = test.location) === null || _a === void 0 ? void 0 : _a.file) && ((_b = test.location) === null || _b === void 0 ? void 0 : _b.line) && ((_c = test.location) === null || _c === void 0 ? void 0 : _c.column)) {
195
+ if (((_c = test.location) === null || _c === void 0 ? void 0 : _c.file) && ((_d = test.location) === null || _d === void 0 ? void 0 : _d.line) && ((_e = test.location) === null || _e === void 0 ? void 0 : _e.column)) {
200
196
  const relativePath = path.relative(this.config.rootDir, test.location.file);
201
197
  codeSnippet = `Test defined at: ${relativePath}:${test.location.line}:${test.location.column}`;
202
198
  }
@@ -209,17 +205,18 @@ class PlaywrightPulseReporter {
209
205
  id: test.id || `${test.title}-${startTime.toISOString()}-${(0, crypto_1.randomUUID)()}`, // Use the original ID logic here
210
206
  runId: "TBD", // Will be set later
211
207
  name: test.titlePath().join(" > "),
212
- suiteName: ((_d = this.config.projects[0]) === null || _d === void 0 ? void 0 : _d.name) || "Default Suite",
208
+ suiteName: ((_f = this.config.projects[0]) === null || _f === void 0 ? void 0 : _f.name) || "Default Suite",
213
209
  status: testStatus,
214
210
  duration: result.duration,
215
211
  startTime: startTime,
216
212
  endTime: endTime,
213
+ browser: browserName,
217
214
  retries: result.retry,
218
- steps: ((_e = result.steps) === null || _e === void 0 ? void 0 : _e.length)
215
+ steps: ((_g = result.steps) === null || _g === void 0 ? void 0 : _g.length)
219
216
  ? await processAllSteps(result.steps, testStatus)
220
217
  : [],
221
- errorMessage: (_f = result.error) === null || _f === void 0 ? void 0 : _f.message,
222
- stackTrace: (_g = result.error) === null || _g === void 0 ? void 0 : _g.stack,
218
+ errorMessage: (_h = result.error) === null || _h === void 0 ? void 0 : _h.message,
219
+ stackTrace: (_j = result.error) === null || _j === void 0 ? void 0 : _j.stack,
223
220
  codeSnippet: codeSnippet,
224
221
  tags: test.tags.map((tag) => tag.startsWith("@") ? tag.substring(1) : tag),
225
222
  screenshots: [],
@@ -7,6 +7,7 @@ export interface TestStep {
7
7
  duration: number;
8
8
  startTime: Date;
9
9
  endTime: Date;
10
+ browser: string;
10
11
  errorMessage?: string;
11
12
  stackTrace?: string;
12
13
  codeLocation?: string;
@@ -29,6 +30,7 @@ export interface TestResult {
29
30
  tags?: string[];
30
31
  suiteName?: string;
31
32
  runId: string;
33
+ browser: string;
32
34
  screenshots?: string[];
33
35
  videoPath?: string;
34
36
  tracePath?: string;
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.1.1",
4
+ "version": "0.1.2",
5
5
  "description": "A Playwright reporter and dashboard for visualizing test results.",
6
6
  "keywords": [
7
7
  "playwright",
@@ -221,7 +221,7 @@ function getSuitesData(results) {
221
221
  const suitesMap = new Map();
222
222
 
223
223
  results.forEach((test) => {
224
- const browser = test.name.split(" > ")[1]; // Extract browser (chromium/firefox/webkit)
224
+ const browser = test.browser; // Extract browser (chromium/firefox/webkit)
225
225
  const suiteName = test.suiteName;
226
226
  const key = `${suiteName}|${browser}`;
227
227
 
@@ -231,6 +231,7 @@ function getSuitesData(results) {
231
231
  name: `${suiteName} (${browser})`,
232
232
  status: test.status,
233
233
  count: 0,
234
+ browser: browser,
234
235
  });
235
236
  }
236
237
  suitesMap.get(key).count++;
@@ -267,6 +268,7 @@ function generateSuitesWidget(suitesData) {
267
268
  suite.count !== 1 ? "s" : ""
268
269
  }</span>
269
270
  </div>
271
+ <span class="browser-name">${suite.browser}</span>
270
272
  </div>
271
273
  `
272
274
  )
@@ -469,8 +471,7 @@ function generateHTML(reportData) {
469
471
 
470
472
  return results
471
473
  .map((test, index) => {
472
- const browserMatch = test.name.match(/ > (\w+) > /);
473
- const browser = browserMatch ? browserMatch[1] : "unknown";
474
+ const browser = test.browser || "unknown";
474
475
  const testName = test.name.split(" > ").pop() || test.name;
475
476
 
476
477
  // Generate steps HTML recursively
@@ -1272,12 +1273,7 @@ function generateHTML(reportData) {
1272
1273
  <select id="filter-browser">
1273
1274
  <option value="">All Browsers</option>
1274
1275
  ${Array.from(
1275
- new Set(
1276
- results.map((test) => {
1277
- const match = test.name.match(/ > (\w+) > /);
1278
- return match ? match[1] : "unknown";
1279
- })
1280
- )
1276
+ new Set(results.map((test) => test.browser || "unknown"))
1281
1277
  )
1282
1278
  .map(
1283
1279
  (browser) => `
@@ -1533,4 +1529,4 @@ async function main() {
1533
1529
  }
1534
1530
 
1535
1531
  // Run the main function
1536
- main();
1532
+ main();