@m00nsolutions/playwright-reporter 1.0.5 → 1.0.7

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 +97 -6
  2. package/index.mjs +150 -9
  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,12 +75,76 @@ 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
82
82
  ```
83
83
 
84
+ ## CI/CD Auto-Detection
85
+
86
+ The reporter **automatically detects** CI/CD environment variables from popular providers and includes them as run attributes. No configuration needed — just run your tests in CI and the dashboard will display branch, commit, build URL, and more.
87
+
88
+ ### Supported Providers
89
+
90
+ | Provider | Detection Variable |
91
+ |---|---|
92
+ | **GitHub Actions** | `GITHUB_ACTIONS` |
93
+ | **GitLab CI** | `GITLAB_CI` |
94
+ | **Jenkins** | `JENKINS_URL` |
95
+ | **Bitbucket Pipelines** | `BITBUCKET_PIPELINE_UUID` |
96
+ | **Azure DevOps** | `TF_BUILD` |
97
+ | **CircleCI** | `CIRCLECI` |
98
+ | **Travis CI** | `TRAVIS` |
99
+
100
+ ### Auto-Detected Attributes
101
+
102
+ The following attributes are automatically populated when running in a supported CI environment:
103
+
104
+ | Attribute | Description | GitHub Actions | GitLab CI | Jenkins | Bitbucket | Azure DevOps | CircleCI | Travis CI |
105
+ |---|---|---|---|---|---|---|---|---|
106
+ | `ci_provider` | CI provider name | `GitHub Actions` | `GitLab CI` | `Jenkins` | `Bitbucket Pipelines` | `Azure DevOps` | `CircleCI` | `Travis CI` |
107
+ | `branch` | Git branch | `GITHUB_REF_NAME` | `CI_COMMIT_REF_NAME` | `BRANCH_NAME` | `BITBUCKET_BRANCH` | `BUILD_SOURCEBRANCH` | `CIRCLE_BRANCH` | `TRAVIS_BRANCH` |
108
+ | `commit` | Git commit SHA | `GITHUB_SHA` | `CI_COMMIT_SHA` | `GIT_COMMIT` | `BITBUCKET_COMMIT` | `BUILD_SOURCEVERSION` | `CIRCLE_SHA1` | `TRAVIS_COMMIT` |
109
+ | `pipeline` | Pipeline/workflow name | `GITHUB_WORKFLOW` | `CI_PIPELINE_NAME` | `JOB_NAME` | — | `BUILD_DEFINITIONNAME` | `CIRCLE_WORKFLOW_JOB_NAME` | — |
110
+ | `build_number` | Build number | `GITHUB_RUN_NUMBER` | `CI_PIPELINE_ID` | `BUILD_NUMBER` | `BITBUCKET_BUILD_NUMBER` | `BUILD_BUILDNUMBER` | `CIRCLE_BUILD_NUM` | `TRAVIS_BUILD_NUMBER` |
111
+ | `build_url` | Link to build | Auto-composed | `CI_PIPELINE_URL` | `BUILD_URL` | Auto-composed | Auto-composed | `CIRCLE_BUILD_URL` | `TRAVIS_BUILD_WEB_URL` |
112
+ | `trigger` | What triggered the build | `GITHUB_EVENT_NAME` | `CI_PIPELINE_SOURCE` | — | — | `BUILD_REASON` | — | `TRAVIS_EVENT_TYPE` |
113
+ | `triggered_by` | User who triggered | `GITHUB_ACTOR` | `CI_GITLAB_USER_LOGIN` | — | — | — | `CIRCLE_USERNAME` | — |
114
+
115
+ ### Manual Override
116
+
117
+ User-provided attributes always take precedence over auto-detected values. To override any auto-detected attribute, simply set it in the `attributes` config:
118
+
119
+ ```typescript
120
+ reporter: [
121
+ ['@m00nsolutions/playwright-reporter', {
122
+ serverUrl: 'https://m00nreport.com',
123
+ apiKey: process.env.M00N_API_KEY,
124
+ attributes: {
125
+ branch: 'my-custom-branch', // Overrides auto-detected branch
126
+ environment: 'staging', // Custom attribute (not auto-detected)
127
+ },
128
+ }],
129
+ ],
130
+ ```
131
+
132
+ ### Custom CI Variables
133
+
134
+ For unsupported CI providers or additional metadata, pass any key-value pairs via `attributes`:
135
+
136
+ ```typescript
137
+ attributes: {
138
+ build_url: process.env.MY_CI_BUILD_URL,
139
+ branch: process.env.MY_CI_BRANCH,
140
+ commit: process.env.MY_CI_COMMIT,
141
+ environment: 'production',
142
+ region: 'us-east-1',
143
+ },
144
+ ```
145
+
146
+ The dashboard recognizes these attribute keys and displays them in the CI/CD banner: `branch`, `commit`, `pipeline`, `build_number`, `build_url`, `environment`, `trigger`.
147
+
84
148
  ## Usage Examples
85
149
 
86
150
  ### Basic Configuration
@@ -88,12 +152,39 @@ M00N_TAGS=smoke,regression
88
152
  ```typescript
89
153
  reporter: [
90
154
  ['@m00nsolutions/playwright-reporter', {
91
- serverUrl: 'https://app.m00n.report',
155
+ serverUrl: 'https://m00nreport.com',
92
156
  apiKey: process.env.M00N_API_KEY,
93
157
  }],
94
158
  ],
95
159
  ```
96
160
 
161
+ ### GitHub Actions Example
162
+
163
+ ```yaml
164
+ # .github/workflows/tests.yml
165
+ jobs:
166
+ test:
167
+ runs-on: ubuntu-latest
168
+ steps:
169
+ - uses: actions/checkout@v4
170
+ - run: npx playwright test
171
+ env:
172
+ M00N_API_KEY: ${{ secrets.M00N_API_KEY }}
173
+ ```
174
+
175
+ The reporter will automatically capture `branch`, `commit`, `build_url`, `pipeline`, `build_number`, `trigger`, and `triggered_by` from GitHub Actions environment variables.
176
+
177
+ ### GitLab CI Example
178
+
179
+ ```yaml
180
+ # .gitlab-ci.yml
181
+ test:
182
+ script:
183
+ - npx playwright test
184
+ variables:
185
+ M00N_API_KEY: $M00N_API_KEY
186
+ ```
187
+
97
188
  ## Real-time Step Streaming
98
189
 
99
190
  When `realtime: true` (default), the reporter streams test steps to the dashboard as they execute. This allows you to:
@@ -132,6 +223,6 @@ MIT License - see [LICENSE](LICENSE) for details.
132
223
 
133
224
  ## Support
134
225
 
135
- - 📖 [Documentation](https://app.m00n.report/documentation)
226
+ - 📖 [Documentation](https://m00nreport.com/documentation)
136
227
  - 🐛 [Report Issues](https://github.com/m00nsolutions/m00nreport/issues)
137
228
  - 💬 [Community Discord](https://discord.gg/hzZvyVWS3Q)
package/index.mjs CHANGED
@@ -38,6 +38,10 @@ 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
@@ -466,6 +470,84 @@ function parseTags(tags) {
466
470
  return [];
467
471
  }
468
472
 
473
+ /**
474
+ * Auto-detect CI/CD environment variables from common providers.
475
+ * Returns a flat key-value object using attribute names that the
476
+ * M00n Report dashboard recognizes (branch, commit, pipeline, etc.).
477
+ * User-provided attributes always take precedence over auto-detected ones.
478
+ */
479
+ function detectCIAttributes() {
480
+ const env = process.env;
481
+ const attrs = {};
482
+
483
+ if (env.GITHUB_ACTIONS) {
484
+ attrs.ci_provider = 'GitHub Actions';
485
+ if (env.GITHUB_WORKFLOW) attrs.pipeline = env.GITHUB_WORKFLOW;
486
+ if (env.GITHUB_RUN_NUMBER) attrs.build_number = env.GITHUB_RUN_NUMBER;
487
+ if (env.GITHUB_SHA) attrs.commit = env.GITHUB_SHA;
488
+ if (env.GITHUB_REF_NAME) attrs.branch = env.GITHUB_REF_NAME;
489
+ else if (env.GITHUB_REF) attrs.branch = env.GITHUB_REF.replace(/^refs\/heads\//, '');
490
+ if (env.GITHUB_EVENT_NAME) attrs.trigger = env.GITHUB_EVENT_NAME;
491
+ if (env.GITHUB_ACTOR) attrs.triggered_by = env.GITHUB_ACTOR;
492
+ if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID) {
493
+ attrs.build_url = `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`;
494
+ }
495
+ } else if (env.GITLAB_CI) {
496
+ attrs.ci_provider = 'GitLab CI';
497
+ if (env.CI_PIPELINE_NAME) attrs.pipeline = env.CI_PIPELINE_NAME;
498
+ if (env.CI_PIPELINE_ID) attrs.build_number = env.CI_PIPELINE_ID;
499
+ if (env.CI_PIPELINE_URL) attrs.build_url = env.CI_PIPELINE_URL;
500
+ if (env.CI_PIPELINE_SOURCE) attrs.trigger = env.CI_PIPELINE_SOURCE;
501
+ if (env.CI_COMMIT_SHA) attrs.commit = env.CI_COMMIT_SHA;
502
+ if (env.CI_COMMIT_REF_NAME) attrs.branch = env.CI_COMMIT_REF_NAME;
503
+ if (env.CI_JOB_URL) attrs.ci_job_url = env.CI_JOB_URL;
504
+ if (env.CI_GITLAB_USER_LOGIN) attrs.triggered_by = env.CI_GITLAB_USER_LOGIN;
505
+ } else if (env.JENKINS_URL) {
506
+ attrs.ci_provider = 'Jenkins';
507
+ if (env.JOB_NAME) attrs.pipeline = env.JOB_NAME;
508
+ if (env.BUILD_NUMBER) attrs.build_number = env.BUILD_NUMBER;
509
+ if (env.BUILD_URL) attrs.build_url = env.BUILD_URL;
510
+ if (env.GIT_COMMIT) attrs.commit = env.GIT_COMMIT;
511
+ if (env.BRANCH_NAME) attrs.branch = env.BRANCH_NAME;
512
+ else if (env.GIT_BRANCH) attrs.branch = env.GIT_BRANCH;
513
+ } else if (env.BITBUCKET_PIPELINE_UUID) {
514
+ attrs.ci_provider = 'Bitbucket Pipelines';
515
+ if (env.BITBUCKET_BUILD_NUMBER) attrs.build_number = env.BITBUCKET_BUILD_NUMBER;
516
+ if (env.BITBUCKET_COMMIT) attrs.commit = env.BITBUCKET_COMMIT;
517
+ if (env.BITBUCKET_BRANCH) attrs.branch = env.BITBUCKET_BRANCH;
518
+ if (env.BITBUCKET_REPO_SLUG && env.BITBUCKET_WORKSPACE && env.BITBUCKET_BUILD_NUMBER) {
519
+ attrs.build_url = `https://bitbucket.org/${env.BITBUCKET_WORKSPACE}/${env.BITBUCKET_REPO_SLUG}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}`;
520
+ }
521
+ } else if (env.TF_BUILD) {
522
+ attrs.ci_provider = 'Azure DevOps';
523
+ if (env.BUILD_DEFINITIONNAME) attrs.pipeline = env.BUILD_DEFINITIONNAME;
524
+ if (env.BUILD_BUILDNUMBER) attrs.build_number = env.BUILD_BUILDNUMBER;
525
+ if (env.BUILD_SOURCEVERSION) attrs.commit = env.BUILD_SOURCEVERSION;
526
+ if (env.BUILD_SOURCEBRANCH) attrs.branch = env.BUILD_SOURCEBRANCH.replace(/^refs\/heads\//, '');
527
+ if (env.BUILD_REASON) attrs.trigger = env.BUILD_REASON;
528
+ if (env.SYSTEM_TEAMFOUNDATIONSERVERURI && env.SYSTEM_TEAMPROJECT && env.BUILD_BUILDID) {
529
+ attrs.build_url = `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`;
530
+ }
531
+ } else if (env.CIRCLECI) {
532
+ attrs.ci_provider = 'CircleCI';
533
+ if (env.CIRCLE_WORKFLOW_JOB_NAME) attrs.pipeline = env.CIRCLE_WORKFLOW_JOB_NAME;
534
+ if (env.CIRCLE_BUILD_NUM) attrs.build_number = env.CIRCLE_BUILD_NUM;
535
+ if (env.CIRCLE_BUILD_URL) attrs.build_url = env.CIRCLE_BUILD_URL;
536
+ if (env.CIRCLE_SHA1) attrs.commit = env.CIRCLE_SHA1;
537
+ if (env.CIRCLE_BRANCH) attrs.branch = env.CIRCLE_BRANCH;
538
+ if (env.CIRCLE_USERNAME) attrs.triggered_by = env.CIRCLE_USERNAME;
539
+ } else if (env.TRAVIS) {
540
+ attrs.ci_provider = 'Travis CI';
541
+ if (env.TRAVIS_BUILD_NUMBER) attrs.build_number = env.TRAVIS_BUILD_NUMBER;
542
+ if (env.TRAVIS_BUILD_WEB_URL) attrs.build_url = env.TRAVIS_BUILD_WEB_URL;
543
+ if (env.TRAVIS_COMMIT) attrs.commit = env.TRAVIS_COMMIT;
544
+ if (env.TRAVIS_BRANCH) attrs.branch = env.TRAVIS_BRANCH;
545
+ if (env.TRAVIS_EVENT_TYPE) attrs.trigger = env.TRAVIS_EVENT_TYPE;
546
+ }
547
+
548
+ return attrs;
549
+ }
550
+
469
551
  function extractAnnotations(test) {
470
552
  const result = {};
471
553
  const caseIdAnn = test?.annotations?.find(a => a?.type === 'caseId')?.description;
@@ -560,9 +642,9 @@ class HttpClient {
560
642
 
561
643
  // Check for permanent errors (don't retry)
562
644
  const errorData = await response.json().catch(() => ({}));
563
- if (['PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY'].includes(errorData.code)) {
645
+ if (['PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY', 'RUN_ATTACHMENT_LIMIT_EXCEEDED'].includes(errorData.code)) {
564
646
  endTiming(0);
565
- return { error: errorData.code, permanent: true };
647
+ return { error: errorData.code, code: errorData.code, permanent: true, ...errorData };
566
648
  }
567
649
 
568
650
  // Transient error - retry if we have attempts left
@@ -734,6 +816,13 @@ class HttpClient {
734
816
  }
735
817
 
736
818
  const errorData = await response.json().catch(() => ({}));
819
+
820
+ // Check for permanent errors (don't retry)
821
+ if (['RUN_ATTACHMENT_LIMIT_EXCEEDED', 'PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY'].includes(errorData.code)) {
822
+ endTiming(0);
823
+ return { error: errorData.code, code: errorData.code, permanent: true, ...errorData };
824
+ }
825
+
737
826
  if (attempt < maxRetries) {
738
827
  this.perfTracker?.recordRetry();
739
828
  if (this.verbose) {
@@ -890,6 +979,13 @@ class HttpClient {
890
979
  }
891
980
 
892
981
  const errorData = await response.json().catch(() => ({}));
982
+
983
+ // Check for permanent errors (don't retry)
984
+ if (['RUN_ATTACHMENT_LIMIT_EXCEEDED', 'PROJECT_NOT_FOUND', 'API_KEY_REQUIRED', 'INVALID_API_KEY'].includes(errorData.code)) {
985
+ endTiming(0);
986
+ return { error: errorData.code, code: errorData.code, permanent: true, ...errorData };
987
+ }
988
+
893
989
  if (attempt < maxRetries) {
894
990
  this.perfTracker?.recordRetry();
895
991
  if (this.verbose) {
@@ -1194,7 +1290,12 @@ class TestCollector {
1194
1290
  // File path - check size to decide streaming vs buffering
1195
1291
  try {
1196
1292
  const stats = await fs.promises.stat(attachment.path);
1197
- if (stats.size > 500 * 1024 * 1024) return null; // Skip >500MB
1293
+ if (stats.size > MAX_ATTACHMENT_SIZE) {
1294
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
1295
+ const limitMB = (MAX_ATTACHMENT_SIZE / 1024 / 1024).toFixed(0);
1296
+ console.warn(`[M00nReporter] Attachment skipped: "${name}" (${sizeMB}MB) exceeds ${limitMB}MB limit. File: ${attachment.path}`);
1297
+ return null;
1298
+ }
1198
1299
 
1199
1300
  if (name === 'trace') name = path.basename(attachment.path);
1200
1301
 
@@ -1365,6 +1466,9 @@ export default class M00nReporter {
1365
1466
  // Prevents server/MinIO overload with high parallelism
1366
1467
  this.uploadSemaphore = new UploadSemaphore(MAX_GLOBAL_UPLOAD_CONCURRENCY);
1367
1468
 
1469
+ // Set when server returns RUN_ATTACHMENT_LIMIT_EXCEEDED -- skip remaining uploads for this run
1470
+ this._runLimitReached = false;
1471
+
1368
1472
  // test/start is now fire-and-forget (no need to track promises)
1369
1473
  // testId is generated client-side, so steps can be streamed immediately
1370
1474
 
@@ -1462,10 +1566,13 @@ export default class M00nReporter {
1462
1566
  return { ok: true };
1463
1567
  }).catch(err => ({ ok: false, error: err }));
1464
1568
 
1465
- // Normalize attributes: flat key-value object
1466
- const attributes = this.opts.attributes && typeof this.opts.attributes === 'object'
1467
- ? { ...this.opts.attributes }
1569
+ // Auto-detect CI environment variables, then overlay user-provided attributes
1570
+ // User attributes always take precedence over auto-detected ones
1571
+ const ciAttrs = detectCIAttributes();
1572
+ const userAttrs = this.opts.attributes && typeof this.opts.attributes === 'object'
1573
+ ? this.opts.attributes
1468
1574
  : {};
1575
+ const attributes = { ...ciAttrs, ...userAttrs };
1469
1576
 
1470
1577
  // Add workers count from Playwright config (useful for timeline visualization)
1471
1578
  if (config.workers != null) {
@@ -1808,9 +1915,17 @@ export default class M00nReporter {
1808
1915
  // Uses semaphore to limit concurrent uploads across ALL tests
1809
1916
  // Returns { uploaded: N, failed: M } for tracking
1810
1917
  async uploadAttachmentsWithBackpressure(runId, testId, attachments) {
1811
- if (!attachments || attachments.length === 0) return { uploaded: 0, failed: 0 };
1918
+ if (!attachments || attachments.length === 0) return { uploaded: 0, failed: 0, skipped: 0 };
1812
1919
 
1813
- const results = { uploaded: 0, failed: 0 };
1920
+ // If a previous upload hit the run attachment limit, skip all remaining uploads
1921
+ if (this._runLimitReached) {
1922
+ if (this.debug) {
1923
+ this.log('debug', `Skipping ${attachments.length} attachment(s) - run attachment limit already reached`);
1924
+ }
1925
+ return { uploaded: 0, failed: 0, skipped: attachments.length };
1926
+ }
1927
+
1928
+ const results = { uploaded: 0, failed: 0, skipped: 0 };
1814
1929
  const uploadStart = performance.now();
1815
1930
 
1816
1931
  // Separate small and large files
@@ -1823,6 +1938,9 @@ export default class M00nReporter {
1823
1938
  // Small files - upload individually with semaphore for better parallelism
1824
1939
  for (const file of smallFiles) {
1825
1940
  uploadTasks.push(this.uploadSemaphore.run(async () => {
1941
+ // Check limit flag before each upload (may have been set by a concurrent upload)
1942
+ if (this._runLimitReached) return 'skipped';
1943
+
1826
1944
  try {
1827
1945
  const resp = await this.http.postMultipart(
1828
1946
  '/api/ingest/v2/attachment/upload',
@@ -1831,6 +1949,13 @@ export default class M00nReporter {
1831
1949
  { timeout: UPLOAD_TIMEOUT }
1832
1950
  );
1833
1951
 
1952
+ if (resp.code === 'RUN_ATTACHMENT_LIMIT_EXCEEDED') {
1953
+ this._runLimitReached = true;
1954
+ const usedMB = resp.currentBytes ? Math.round(resp.currentBytes / 1024 / 1024) : '?';
1955
+ const limitMB = resp.limitBytes ? Math.round(resp.limitBytes / 1024 / 1024) : '?';
1956
+ this.log('warn', `Attachment skipped: run attachment limit reached (${usedMB}/${limitMB} MB used). Remaining attachments for this run will be skipped.`);
1957
+ return 'skipped';
1958
+ }
1834
1959
  if (resp.error) {
1835
1960
  this.addError(`Upload failed (${file.name}): ${resp.error}`);
1836
1961
  return false;
@@ -1846,6 +1971,9 @@ export default class M00nReporter {
1846
1971
  // Large files - stream upload with semaphore
1847
1972
  for (const largeFile of largeFiles) {
1848
1973
  uploadTasks.push(this.uploadSemaphore.run(async () => {
1974
+ // Check limit flag before each upload (may have been set by a concurrent upload)
1975
+ if (this._runLimitReached) return 'skipped';
1976
+
1849
1977
  try {
1850
1978
  if (this.debug) {
1851
1979
  this.log('debug', `Streaming large file: ${largeFile.name} (${(largeFile.size / 1024 / 1024).toFixed(1)}MB)`);
@@ -1860,6 +1988,13 @@ export default class M00nReporter {
1860
1988
  { timeout: STREAM_UPLOAD_TIMEOUT }
1861
1989
  );
1862
1990
 
1991
+ if (resp.code === 'RUN_ATTACHMENT_LIMIT_EXCEEDED') {
1992
+ this._runLimitReached = true;
1993
+ const usedMB = resp.currentBytes ? Math.round(resp.currentBytes / 1024 / 1024) : '?';
1994
+ const limitMB = resp.limitBytes ? Math.round(resp.limitBytes / 1024 / 1024) : '?';
1995
+ this.log('warn', `Attachment skipped: run attachment limit reached (${usedMB}/${limitMB} MB used). Remaining attachments for this run will be skipped.`);
1996
+ return 'skipped';
1997
+ }
1863
1998
  if (resp.error) {
1864
1999
  this.addError(`Stream upload failed (${largeFile.name}): ${resp.error}`);
1865
2000
  return false;
@@ -1879,6 +2014,8 @@ export default class M00nReporter {
1879
2014
  for (const result of uploadResults) {
1880
2015
  if (result.status === 'fulfilled' && result.value === true) {
1881
2016
  results.uploaded++;
2017
+ } else if (result.status === 'fulfilled' && result.value === 'skipped') {
2018
+ results.skipped++;
1882
2019
  } else {
1883
2020
  results.failed++;
1884
2021
  }
@@ -1887,7 +2024,9 @@ export default class M00nReporter {
1887
2024
  const uploadDuration = performance.now() - uploadStart;
1888
2025
 
1889
2026
  if (this.verbose) {
1890
- this.log('perf', `Attachments uploaded: ${results.uploaded}/${attachments.length} in ${Math.round(uploadDuration)}ms`);
2027
+ const parts = [`${results.uploaded}/${attachments.length} in ${Math.round(uploadDuration)}ms`];
2028
+ if (results.skipped > 0) parts.push(`${results.skipped} skipped (limit reached)`);
2029
+ this.log('perf', `Attachments uploaded: ${parts.join(', ')}`);
1891
2030
  }
1892
2031
 
1893
2032
  // Log to file
@@ -1896,6 +2035,8 @@ export default class M00nReporter {
1896
2035
  total: attachments.length,
1897
2036
  uploaded: results.uploaded,
1898
2037
  failed: results.failed,
2038
+ skipped: results.skipped,
2039
+ limitReached: this._runLimitReached,
1899
2040
  duration: Math.round(uploadDuration),
1900
2041
  });
1901
2042
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m00nsolutions/playwright-reporter",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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://app.m00n.report",
45
+ "homepage": "https://m00nreport.com",
46
46
  "bugs": {
47
47
  "url": "https://github.com/m00nsolutions/m00nreport/issues"
48
48
  },