@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.
- package/README.md +97 -6
- package/index.mjs +150 -9
- 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://
|
|
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 [
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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 >
|
|
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
|
-
//
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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://
|
|
45
|
+
"homepage": "https://m00nreport.com",
|
|
46
46
|
"bugs": {
|
|
47
47
|
"url": "https://github.com/m00nsolutions/m00nreport/issues"
|
|
48
48
|
},
|