@m00nsolutions/playwright-reporter 1.0.4 → 1.0.6

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.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/index.mjs +86 -14
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @m00nsolutions/playwright-reporter
2
2
 
3
- Official Playwright test reporter for [M00n Report](https://app.m00n.report) - a real-time test reporting dashboard.
3
+ Official Playwright test reporter for [M00n Report](https://m00nreport.com) - a real-time test reporting dashboard.
4
4
 
5
5
  ## Features
6
6
 
@@ -24,7 +24,7 @@ pnpm add @m00nsolutions/playwright-reporter --save-dev
24
24
 
25
25
  ### 1. Get your API key
26
26
 
27
- 1. Log in to [app.m00n.report](https://app.m00n.report)
27
+ 1. Log in to [m00nreport.com](https://m00nreport.com)
28
28
  2. Navigate to your project settings
29
29
  3. Generate or copy your project API key
30
30
 
@@ -39,7 +39,7 @@ export default defineConfig({
39
39
  reporter: [
40
40
  ['list'], // Keep default console output
41
41
  ['@m00nsolutions/playwright-reporter', {
42
- serverUrl: 'https://app.m00n.report', // Or your self-hosted URL
42
+ serverUrl: 'https://m00nreport.com', // Or your self-hosted URL
43
43
  apiKey: process.env.M00N_API_KEY, // Your project API key
44
44
  launch: 'Regression Suite', // Optional: run title
45
45
  tags: ['smoke', 'regression'], // Optional: tags
@@ -75,7 +75,7 @@ That's it! Your test results will appear in the M00n Report dashboard in real-ti
75
75
  You can also configure the reporter via environment variables:
76
76
 
77
77
  ```bash
78
- M00N_SERVER_URL=https://app.m00n.report
78
+ M00N_SERVER_URL=https://m00nreport.com
79
79
  M00N_API_KEY=m00n_xxxxxxxxxxxxx
80
80
  M00N_LAUNCH="Nightly Build"
81
81
  M00N_TAGS=smoke,regression
@@ -88,7 +88,7 @@ M00N_TAGS=smoke,regression
88
88
  ```typescript
89
89
  reporter: [
90
90
  ['@m00nsolutions/playwright-reporter', {
91
- serverUrl: 'https://app.m00n.report',
91
+ serverUrl: 'https://m00nreport.com',
92
92
  apiKey: process.env.M00N_API_KEY,
93
93
  }],
94
94
  ],
@@ -132,6 +132,6 @@ MIT License - see [LICENSE](LICENSE) for details.
132
132
 
133
133
  ## Support
134
134
 
135
- - 📖 [Documentation](https://docs.m00n.report)
135
+ - 📖 [Documentation](https://m00nreport.com/documentation)
136
136
  - 🐛 [Report Issues](https://github.com/m00nsolutions/m00nreport/issues)
137
- - 💬 [Community Discord](https://discord.gg/m00nreport)
137
+ - 💬 [Community Discord](https://discord.gg/hzZvyVWS3Q)
package/index.mjs CHANGED
@@ -38,22 +38,29 @@ import { fileURLToPath } from 'url';
38
38
  // CONSTANTS
39
39
  // ============================================================================
40
40
 
41
+ // Maximum attachment file size - files larger than this are skipped
42
+ // 200MB covers all realistic test artifacts (traces, videos, screenshots)
43
+ const MAX_ATTACHMENT_SIZE = 200 * 1024 * 1024; // 200MB
44
+
41
45
  // Files larger than this threshold will be streamed directly to server
42
46
  // to avoid memory issues in reporter process
43
47
  const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB
44
48
 
45
49
  // Maximum concurrent attachment uploads across ALL tests
46
50
  // Prevents server/MinIO overload with high parallelism (30+ workers)
47
- const MAX_GLOBAL_UPLOAD_CONCURRENCY = 10;
51
+ // Increased from 10 to 15 to reduce queue time when many tests have attachments
52
+ const MAX_GLOBAL_UPLOAD_CONCURRENCY = 15;
48
53
 
49
54
  // Timeout for individual attachment uploads (ms)
50
- const UPLOAD_TIMEOUT = 60000; // 60 seconds
55
+ // Increased from 60s to 90s to handle larger files under load
56
+ const UPLOAD_TIMEOUT = 90000; // 90 seconds
51
57
 
52
58
  // Timeout for streaming large file uploads (ms)
53
59
  const STREAM_UPLOAD_TIMEOUT = 300000; // 5 minutes
54
60
 
55
61
  // Timeout for waiting on pending uploads at run end (ms)
56
- const END_UPLOAD_WAIT_TIMEOUT = 180000; // 3 minutes
62
+ // Increased from 3 to 5 minutes to handle many failing tests with videos
63
+ const END_UPLOAD_WAIT_TIMEOUT = 300000; // 5 minutes
57
64
 
58
65
  // ============================================================================
59
66
  // HELPERS
@@ -557,9 +564,9 @@ class HttpClient {
557
564
 
558
565
  // Check for permanent errors (don't retry)
559
566
  const errorData = await response.json().catch(() => ({}));
560
- if (['PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY'].includes(errorData.code)) {
567
+ if (['PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY', 'RUN_ATTACHMENT_LIMIT_EXCEEDED'].includes(errorData.code)) {
561
568
  endTiming(0);
562
- return { error: errorData.code, permanent: true };
569
+ return { error: errorData.code, code: errorData.code, permanent: true, ...errorData };
563
570
  }
564
571
 
565
572
  // Transient error - retry if we have attempts left
@@ -731,6 +738,13 @@ class HttpClient {
731
738
  }
732
739
 
733
740
  const errorData = await response.json().catch(() => ({}));
741
+
742
+ // Check for permanent errors (don't retry)
743
+ if (['RUN_ATTACHMENT_LIMIT_EXCEEDED', 'PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY'].includes(errorData.code)) {
744
+ endTiming(0);
745
+ return { error: errorData.code, code: errorData.code, permanent: true, ...errorData };
746
+ }
747
+
734
748
  if (attempt < maxRetries) {
735
749
  this.perfTracker?.recordRetry();
736
750
  if (this.verbose) {
@@ -887,6 +901,13 @@ class HttpClient {
887
901
  }
888
902
 
889
903
  const errorData = await response.json().catch(() => ({}));
904
+
905
+ // Check for permanent errors (don't retry)
906
+ if (['RUN_ATTACHMENT_LIMIT_EXCEEDED', 'PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY'].includes(errorData.code)) {
907
+ endTiming(0);
908
+ return { error: errorData.code, code: errorData.code, permanent: true, ...errorData };
909
+ }
910
+
890
911
  if (attempt < maxRetries) {
891
912
  this.perfTracker?.recordRetry();
892
913
  if (this.verbose) {
@@ -1191,7 +1212,12 @@ class TestCollector {
1191
1212
  // File path - check size to decide streaming vs buffering
1192
1213
  try {
1193
1214
  const stats = await fs.promises.stat(attachment.path);
1194
- if (stats.size > 500 * 1024 * 1024) return null; // Skip >500MB
1215
+ if (stats.size > MAX_ATTACHMENT_SIZE) {
1216
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
1217
+ const limitMB = (MAX_ATTACHMENT_SIZE / 1024 / 1024).toFixed(0);
1218
+ console.warn(`[M00nReporter] Attachment skipped: "${name}" (${sizeMB}MB) exceeds ${limitMB}MB limit. File: ${attachment.path}`);
1219
+ return null;
1220
+ }
1195
1221
 
1196
1222
  if (name === 'trace') name = path.basename(attachment.path);
1197
1223
 
@@ -1237,8 +1263,15 @@ class TestCollector {
1237
1263
  }
1238
1264
  } catch (fileErr) {
1239
1265
  // File doesn't exist or can't be read
1240
- // This can happen with 'retain-on-failure' when Playwright cleans up
1241
- // temporary files from non-final retry attempts
1266
+ // This commonly happens with videos when:
1267
+ // 1. 'retain-on-failure' cleans up temporary files from non-final retry attempts
1268
+ // 2. Race condition where video file is deleted before reporter reads it
1269
+ // 3. Disk space issues during high-concurrency runs
1270
+ if (fileErr.code === 'ENOENT') {
1271
+ console.warn(`[M00nReporter] Video/attachment file not found: ${attachment.path} - file may have been cleaned up by Playwright`);
1272
+ } else {
1273
+ console.warn(`[M00nReporter] Failed to read attachment file ${attachment.path}: ${fileErr.message}`);
1274
+ }
1242
1275
  return null;
1243
1276
  }
1244
1277
  }
@@ -1355,6 +1388,9 @@ export default class M00nReporter {
1355
1388
  // Prevents server/MinIO overload with high parallelism
1356
1389
  this.uploadSemaphore = new UploadSemaphore(MAX_GLOBAL_UPLOAD_CONCURRENCY);
1357
1390
 
1391
+ // Set when server returns RUN_ATTACHMENT_LIMIT_EXCEEDED -- skip remaining uploads for this run
1392
+ this._runLimitReached = false;
1393
+
1358
1394
  // test/start is now fire-and-forget (no need to track promises)
1359
1395
  // testId is generated client-side, so steps can be streamed immediately
1360
1396
 
@@ -1674,9 +1710,8 @@ export default class M00nReporter {
1674
1710
  attachment => collector.addAttachment(attachment, collector.testId, this.runId, this.binaryAttachments)
1675
1711
  .catch(err => {
1676
1712
  // Don't let attachment read errors crash the test
1677
- if (this.debug) {
1678
- this.log('debug', `Failed to read attachment: ${err.message}`);
1679
- }
1713
+ // Always log warning for attachment failures (helps diagnose missing videos)
1714
+ this.log('warn', `Failed to read attachment "${attachment.name || 'unknown'}": ${err.message}`);
1680
1715
  return null;
1681
1716
  })
1682
1717
  );
@@ -1799,9 +1834,17 @@ export default class M00nReporter {
1799
1834
  // Uses semaphore to limit concurrent uploads across ALL tests
1800
1835
  // Returns { uploaded: N, failed: M } for tracking
1801
1836
  async uploadAttachmentsWithBackpressure(runId, testId, attachments) {
1802
- if (!attachments || attachments.length === 0) return { uploaded: 0, failed: 0 };
1837
+ if (!attachments || attachments.length === 0) return { uploaded: 0, failed: 0, skipped: 0 };
1838
+
1839
+ // If a previous upload hit the run attachment limit, skip all remaining uploads
1840
+ if (this._runLimitReached) {
1841
+ if (this.debug) {
1842
+ this.log('debug', `Skipping ${attachments.length} attachment(s) - run attachment limit already reached`);
1843
+ }
1844
+ return { uploaded: 0, failed: 0, skipped: attachments.length };
1845
+ }
1803
1846
 
1804
- const results = { uploaded: 0, failed: 0 };
1847
+ const results = { uploaded: 0, failed: 0, skipped: 0 };
1805
1848
  const uploadStart = performance.now();
1806
1849
 
1807
1850
  // Separate small and large files
@@ -1814,6 +1857,9 @@ export default class M00nReporter {
1814
1857
  // Small files - upload individually with semaphore for better parallelism
1815
1858
  for (const file of smallFiles) {
1816
1859
  uploadTasks.push(this.uploadSemaphore.run(async () => {
1860
+ // Check limit flag before each upload (may have been set by a concurrent upload)
1861
+ if (this._runLimitReached) return 'skipped';
1862
+
1817
1863
  try {
1818
1864
  const resp = await this.http.postMultipart(
1819
1865
  '/api/ingest/v2/attachment/upload',
@@ -1822,6 +1868,13 @@ export default class M00nReporter {
1822
1868
  { timeout: UPLOAD_TIMEOUT }
1823
1869
  );
1824
1870
 
1871
+ if (resp.code === 'RUN_ATTACHMENT_LIMIT_EXCEEDED') {
1872
+ this._runLimitReached = true;
1873
+ const usedMB = resp.currentBytes ? Math.round(resp.currentBytes / 1024 / 1024) : '?';
1874
+ const limitMB = resp.limitBytes ? Math.round(resp.limitBytes / 1024 / 1024) : '?';
1875
+ this.log('warn', `Attachment skipped: run attachment limit reached (${usedMB}/${limitMB} MB used). Remaining attachments for this run will be skipped.`);
1876
+ return 'skipped';
1877
+ }
1825
1878
  if (resp.error) {
1826
1879
  this.addError(`Upload failed (${file.name}): ${resp.error}`);
1827
1880
  return false;
@@ -1837,6 +1890,9 @@ export default class M00nReporter {
1837
1890
  // Large files - stream upload with semaphore
1838
1891
  for (const largeFile of largeFiles) {
1839
1892
  uploadTasks.push(this.uploadSemaphore.run(async () => {
1893
+ // Check limit flag before each upload (may have been set by a concurrent upload)
1894
+ if (this._runLimitReached) return 'skipped';
1895
+
1840
1896
  try {
1841
1897
  if (this.debug) {
1842
1898
  this.log('debug', `Streaming large file: ${largeFile.name} (${(largeFile.size / 1024 / 1024).toFixed(1)}MB)`);
@@ -1851,6 +1907,13 @@ export default class M00nReporter {
1851
1907
  { timeout: STREAM_UPLOAD_TIMEOUT }
1852
1908
  );
1853
1909
 
1910
+ if (resp.code === 'RUN_ATTACHMENT_LIMIT_EXCEEDED') {
1911
+ this._runLimitReached = true;
1912
+ const usedMB = resp.currentBytes ? Math.round(resp.currentBytes / 1024 / 1024) : '?';
1913
+ const limitMB = resp.limitBytes ? Math.round(resp.limitBytes / 1024 / 1024) : '?';
1914
+ this.log('warn', `Attachment skipped: run attachment limit reached (${usedMB}/${limitMB} MB used). Remaining attachments for this run will be skipped.`);
1915
+ return 'skipped';
1916
+ }
1854
1917
  if (resp.error) {
1855
1918
  this.addError(`Stream upload failed (${largeFile.name}): ${resp.error}`);
1856
1919
  return false;
@@ -1870,6 +1933,8 @@ export default class M00nReporter {
1870
1933
  for (const result of uploadResults) {
1871
1934
  if (result.status === 'fulfilled' && result.value === true) {
1872
1935
  results.uploaded++;
1936
+ } else if (result.status === 'fulfilled' && result.value === 'skipped') {
1937
+ results.skipped++;
1873
1938
  } else {
1874
1939
  results.failed++;
1875
1940
  }
@@ -1878,7 +1943,9 @@ export default class M00nReporter {
1878
1943
  const uploadDuration = performance.now() - uploadStart;
1879
1944
 
1880
1945
  if (this.verbose) {
1881
- this.log('perf', `Attachments uploaded: ${results.uploaded}/${attachments.length} in ${Math.round(uploadDuration)}ms`);
1946
+ const parts = [`${results.uploaded}/${attachments.length} in ${Math.round(uploadDuration)}ms`];
1947
+ if (results.skipped > 0) parts.push(`${results.skipped} skipped (limit reached)`);
1948
+ this.log('perf', `Attachments uploaded: ${parts.join(', ')}`);
1882
1949
  }
1883
1950
 
1884
1951
  // Log to file
@@ -1887,6 +1954,8 @@ export default class M00nReporter {
1887
1954
  total: attachments.length,
1888
1955
  uploaded: results.uploaded,
1889
1956
  failed: results.failed,
1957
+ skipped: results.skipped,
1958
+ limitReached: this._runLimitReached,
1890
1959
  duration: Math.round(uploadDuration),
1891
1960
  });
1892
1961
 
@@ -1988,6 +2057,9 @@ export default class M00nReporter {
1988
2057
  const attachmentWaitDuration = Math.round(performance.now() - attachmentWaitStart);
1989
2058
 
1990
2059
  if (uploadWaitResult === 'timeout') {
2060
+ const timeoutMinutes = Math.round(END_UPLOAD_WAIT_TIMEOUT / 60000);
2061
+ this.log('warn', `Timeout waiting for attachment uploads after ${timeoutMinutes} minutes - some attachments (videos/traces) may be missing!`);
2062
+ this.log('warn', `Tip: If this happens often, consider reducing video size or increasing END_UPLOAD_WAIT_TIMEOUT`);
1991
2063
  this.addError(`Timeout waiting for attachment uploads after ${END_UPLOAD_WAIT_TIMEOUT/1000}s - some attachments may be missing`);
1992
2064
  this.fileLogger?.event('ATTACHMENTS_WAIT_TIMEOUT', {
1993
2065
  duration: attachmentWaitDuration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m00nsolutions/playwright-reporter",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Playwright test reporter for M00n Report dashboard - real-time test result streaming with step tracking, attachments, and retry support",
5
5
  "main": "index.mjs",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "url": "https://github.com/m00nsolutions/m00nreport.git",
43
43
  "directory": "packages/m00n-playwright-reporter"
44
44
  },
45
- "homepage": "https://m00n.report",
45
+ "homepage": "https://m00nreport.com",
46
46
  "bugs": {
47
47
  "url": "https://github.com/m00nsolutions/m00nreport/issues"
48
48
  },