@stanterprise/playwright-reporter 0.0.1
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/LICENSE +15 -0
- package/README.md +279 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +10 -0
- package/dist/reporter.d.ts +36 -0
- package/dist/reporter.js +456 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +5 -0
- package/dist/utils/attachmentProcessor.d.ts +19 -0
- package/dist/utils/attachmentProcessor.js +44 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +22 -0
- package/dist/utils/statusMapper.d.ts +12 -0
- package/dist/utils/statusMapper.js +29 -0
- package/dist/utils/suiteMapper.d.ts +1 -0
- package/dist/utils/suiteMapper.js +4 -0
- package/dist/utils/timeHelpers.d.ts +26 -0
- package/dist/utils/timeHelpers.js +45 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stanterprise Team
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# stanterprise-playwright-reporter
|
|
2
|
+
|
|
3
|
+
Custom Playwright test reporter that sends test results to Stanterprise via gRPC.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📊 **Comprehensive Test Reporting**: Reports test lifecycle events including begin, end, and failures
|
|
8
|
+
- 🔄 **Step Tracking**: Tracks and reports individual test steps with timing and status
|
|
9
|
+
- 📎 **Attachment Support**: Handles screenshots, videos, and other test attachments
|
|
10
|
+
- 🔌 **gRPC Integration**: Communicates with Stanterprise backend via gRPC protocol
|
|
11
|
+
- ⚙️ **Configurable**: Supports environment variables and configuration options
|
|
12
|
+
- 🛡️ **Error Resilient**: Graceful error handling that doesn't interrupt test execution
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install stanterprise-playwright-reporter --save-dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
### Basic Setup
|
|
23
|
+
|
|
24
|
+
Add the reporter to your `playwright.config.ts`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { defineConfig } from "@playwright/test";
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
reporter: [["stanterprise-playwright-reporter"]],
|
|
31
|
+
// ... other config
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Advanced Configuration
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { defineConfig } from "@playwright/test";
|
|
39
|
+
|
|
40
|
+
export default defineConfig({
|
|
41
|
+
reporter: [
|
|
42
|
+
[
|
|
43
|
+
"stanterprise-playwright-reporter",
|
|
44
|
+
{
|
|
45
|
+
grpcAddress: "localhost:50051", // gRPC server address
|
|
46
|
+
grpcEnabled: true, // Enable/disable gRPC reporting
|
|
47
|
+
grpcTimeout: 1000, // Timeout for gRPC calls in milliseconds
|
|
48
|
+
verbose: false, // Enable verbose logging
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
],
|
|
52
|
+
// ... other config
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Environment Variables
|
|
57
|
+
|
|
58
|
+
You can also configure the reporter using environment variables:
|
|
59
|
+
|
|
60
|
+
- `STANTERPRISE_GRPC_ADDRESS`: gRPC server address (default: `localhost:50051`)
|
|
61
|
+
- `STANTERPRISE_GRPC_ENABLED`: Enable/disable gRPC reporting (default: `true`)
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
STANTERPRISE_GRPC_ADDRESS=myserver.com:50051 npx playwright test
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration Options
|
|
70
|
+
|
|
71
|
+
| Option | Type | Default | Description |
|
|
72
|
+
| -------------- | ------- | ---------------- | ----------------------------------------- |
|
|
73
|
+
| `grpcAddress` | string | `localhost:50051`| gRPC server address |
|
|
74
|
+
| `grpcEnabled` | boolean | `true` | Enable/disable gRPC reporting |
|
|
75
|
+
| `grpcTimeout` | number | `1000` | Timeout for gRPC calls in milliseconds |
|
|
76
|
+
| `verbose` | boolean | `false` | Enable verbose logging |
|
|
77
|
+
|
|
78
|
+
## What Gets Reported
|
|
79
|
+
|
|
80
|
+
### Test Events
|
|
81
|
+
|
|
82
|
+
- **Test Begin**: When a test starts
|
|
83
|
+
- **Test End**: When a test completes with status (passed/failed/skipped/timedOut)
|
|
84
|
+
- **Test Failure**: Detailed failure information including error messages and stack traces
|
|
85
|
+
|
|
86
|
+
### Step Events
|
|
87
|
+
|
|
88
|
+
- **Step Begin**: When a test step starts
|
|
89
|
+
- **Step End**: When a test step completes with duration and status
|
|
90
|
+
|
|
91
|
+
### Attachments
|
|
92
|
+
|
|
93
|
+
The reporter automatically processes and sends:
|
|
94
|
+
- Screenshots
|
|
95
|
+
- Videos
|
|
96
|
+
- Trace files
|
|
97
|
+
- Any other attachments captured during test execution
|
|
98
|
+
|
|
99
|
+
## Usage Example
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// playwright.config.ts
|
|
103
|
+
import { defineConfig } from "@playwright/test";
|
|
104
|
+
|
|
105
|
+
export default defineConfig({
|
|
106
|
+
use: {
|
|
107
|
+
screenshot: "only-on-failure",
|
|
108
|
+
video: "retain-on-failure",
|
|
109
|
+
},
|
|
110
|
+
reporter: [
|
|
111
|
+
["list"], // Also show in console
|
|
112
|
+
[
|
|
113
|
+
"stanterprise-playwright-reporter",
|
|
114
|
+
{
|
|
115
|
+
grpcAddress: process.env.STANTERPRISE_GRPC_ADDRESS || "localhost:50051",
|
|
116
|
+
verbose: process.env.CI === "true",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API
|
|
124
|
+
|
|
125
|
+
### StanterpriseReporter
|
|
126
|
+
|
|
127
|
+
Main reporter class implementing Playwright's `Reporter` interface.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { StanterpriseReporter } from "stanterprise-playwright-reporter";
|
|
131
|
+
|
|
132
|
+
const reporter = new StanterpriseReporter({
|
|
133
|
+
grpcAddress: "localhost:50051",
|
|
134
|
+
grpcEnabled: true,
|
|
135
|
+
grpcTimeout: 1000,
|
|
136
|
+
verbose: false,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Types
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import type {
|
|
144
|
+
StanterpriseReporterOptions,
|
|
145
|
+
TestExecutionContext,
|
|
146
|
+
StepExecutionContext,
|
|
147
|
+
} from "stanterprise-playwright-reporter";
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
### Building
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm run build
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Testing
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm test
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Cleaning
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run clean
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## How It Works
|
|
171
|
+
|
|
172
|
+
1. **Test Run Initialization**: When tests start, the reporter generates a unique run ID and establishes a gRPC connection
|
|
173
|
+
2. **Event Reporting**: As tests execute, the reporter sends events to the Stanterprise backend via gRPC
|
|
174
|
+
3. **Fire-and-Forget**: Events are sent asynchronously to avoid slowing down test execution
|
|
175
|
+
4. **Error Handling**: Connection errors are logged once and further attempts are disabled for that run
|
|
176
|
+
5. **Cleanup**: When tests complete, the gRPC connection is properly closed
|
|
177
|
+
|
|
178
|
+
## Architecture
|
|
179
|
+
|
|
180
|
+
The reporter is organized into several modules:
|
|
181
|
+
|
|
182
|
+
- **reporter.ts**: Main reporter implementation
|
|
183
|
+
- **types.ts**: TypeScript type definitions and interfaces
|
|
184
|
+
- **utils/statusMapper.ts**: Maps Playwright statuses to protobuf enums
|
|
185
|
+
- **utils/attachmentProcessor.ts**: Processes test attachments
|
|
186
|
+
- **utils/timeHelpers.ts**: Handles timestamp and duration conversions
|
|
187
|
+
|
|
188
|
+
## Troubleshooting
|
|
189
|
+
|
|
190
|
+
### Connection Issues
|
|
191
|
+
|
|
192
|
+
If you see gRPC connection errors:
|
|
193
|
+
|
|
194
|
+
1. Verify the gRPC server is running and accessible
|
|
195
|
+
2. Check the `grpcAddress` configuration
|
|
196
|
+
3. Ensure firewall rules allow the connection
|
|
197
|
+
4. Enable verbose logging to see detailed error messages
|
|
198
|
+
|
|
199
|
+
### Reporter Not Working
|
|
200
|
+
|
|
201
|
+
1. Verify the reporter is properly configured in `playwright.config.ts`
|
|
202
|
+
2. Check that `grpcEnabled` is not set to `false`
|
|
203
|
+
3. Look for error messages in the test output
|
|
204
|
+
4. Try with `verbose: true` to see detailed logs
|
|
205
|
+
|
|
206
|
+
## Contributing
|
|
207
|
+
|
|
208
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
209
|
+
|
|
210
|
+
## For Maintainers
|
|
211
|
+
|
|
212
|
+
### Publishing to NPM
|
|
213
|
+
|
|
214
|
+
#### Automated Publishing (Recommended)
|
|
215
|
+
|
|
216
|
+
The repository uses GitHub Actions to automatically publish to NPM when a version tag is pushed:
|
|
217
|
+
|
|
218
|
+
1. Update the version following [semantic versioning](https://semver.org/):
|
|
219
|
+
```bash
|
|
220
|
+
npm version patch # for bug fixes (1.0.0 → 1.0.1)
|
|
221
|
+
npm version minor # for new features (1.0.0 → 1.1.0)
|
|
222
|
+
npm version major # for breaking changes (1.0.0 → 2.0.0)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
2. Push the version tag to GitHub:
|
|
226
|
+
```bash
|
|
227
|
+
git push --follow-tags
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
3. The GitHub Actions workflow will automatically:
|
|
231
|
+
- Run tests
|
|
232
|
+
- Build the package
|
|
233
|
+
- Publish to NPM with provenance
|
|
234
|
+
|
|
235
|
+
**Note**: Requires `NPM_TOKEN` secret to be configured in GitHub repository settings with a valid NPM access token.
|
|
236
|
+
|
|
237
|
+
#### Manual Publishing
|
|
238
|
+
|
|
239
|
+
Alternatively, you can publish manually:
|
|
240
|
+
|
|
241
|
+
1. Ensure all tests pass:
|
|
242
|
+
```bash
|
|
243
|
+
npm test
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
2. Update the version:
|
|
247
|
+
```bash
|
|
248
|
+
npm version patch|minor|major
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
3. Publish to NPM (prepublishOnly script will build automatically):
|
|
252
|
+
```bash
|
|
253
|
+
npm publish
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
4. Push the tag to GitHub:
|
|
257
|
+
```bash
|
|
258
|
+
git push --follow-tags
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### What Gets Published
|
|
262
|
+
|
|
263
|
+
The package includes:
|
|
264
|
+
- `dist/` - Compiled JavaScript and TypeScript declarations
|
|
265
|
+
- `README.md` - Documentation
|
|
266
|
+
|
|
267
|
+
The following are excluded via `.npmignore`:
|
|
268
|
+
- Source files (`src/`, `tests/`, `examples/`)
|
|
269
|
+
- Development configuration files
|
|
270
|
+
- Build artifacts and logs
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
ISC - See [LICENSE](LICENSE) file for details
|
|
275
|
+
|
|
276
|
+
## Support
|
|
277
|
+
|
|
278
|
+
For issues and questions, please visit the [GitHub repository](https://github.com/stanterprise/stanterprise-playwright-reporter).
|
|
279
|
+
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StanterpriseReporter = exports.default = void 0;
|
|
7
|
+
var reporter_1 = require("./reporter");
|
|
8
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(reporter_1).default; } });
|
|
9
|
+
var reporter_2 = require("./reporter");
|
|
10
|
+
Object.defineProperty(exports, "StanterpriseReporter", { enumerable: true, get: function () { return __importDefault(reporter_2).default; } });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult, TestStep, TestError } from "@playwright/test/reporter";
|
|
2
|
+
import { StanterpriseReporterOptions } from "./types";
|
|
3
|
+
export default class StanterpriseReporter implements Reporter {
|
|
4
|
+
private grpcClient;
|
|
5
|
+
private grpcAddress;
|
|
6
|
+
private grpcEnabled;
|
|
7
|
+
private grpcTimeout;
|
|
8
|
+
private grpcFirstErrorLogged;
|
|
9
|
+
private verbose;
|
|
10
|
+
private runId;
|
|
11
|
+
private runStartTime;
|
|
12
|
+
private rootSuite;
|
|
13
|
+
private reportedSuites;
|
|
14
|
+
constructor(options?: StanterpriseReporterOptions);
|
|
15
|
+
onBegin(config: FullConfig, suite: Suite): void;
|
|
16
|
+
onExit(): Promise<void>;
|
|
17
|
+
getRunId(): string;
|
|
18
|
+
getRunStartTime(): Date;
|
|
19
|
+
onEnd(result: FullResult): Promise<{
|
|
20
|
+
status?: FullResult["status"];
|
|
21
|
+
} | undefined | void> | void;
|
|
22
|
+
onTestBegin(test: TestCase, result: TestResult): void;
|
|
23
|
+
onStepBegin(test: TestCase, result: TestResult, step: TestStep): Promise<void>;
|
|
24
|
+
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void;
|
|
25
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
26
|
+
onTestFail(test: TestCase, result: TestResult): void;
|
|
27
|
+
onError(error: TestError): void;
|
|
28
|
+
onStdErr(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void;
|
|
29
|
+
onStdOut(chunk: string | Buffer, test: void | TestCase, result: void | TestResult): void;
|
|
30
|
+
private getSuiteId;
|
|
31
|
+
private reportSuiteBegin;
|
|
32
|
+
private reportSuiteEnd;
|
|
33
|
+
private mapSuiteStatus;
|
|
34
|
+
private logGrpcErrorOnce;
|
|
35
|
+
private reportUnary;
|
|
36
|
+
}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const protobuf_1 = require("@stanterprise/protobuf");
|
|
37
|
+
const grpc = __importStar(require("@grpc/grpc-js"));
|
|
38
|
+
const crypto_1 = require("crypto");
|
|
39
|
+
const utils_1 = require("./utils");
|
|
40
|
+
// Create shortcuts for the protobuf classes
|
|
41
|
+
const EventsNS = protobuf_1.events.v1.events;
|
|
42
|
+
const TestCaseEntities = protobuf_1.testCase.v1.entities;
|
|
43
|
+
const TestSuiteEntities = protobuf_1.testSuite.v1.entities;
|
|
44
|
+
const TestStatus = protobuf_1.common.v1.common.TestStatus;
|
|
45
|
+
class StanterpriseReporter {
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
// Generic gRPC client (we call unary methods by path directly).
|
|
48
|
+
this.grpcClient = null;
|
|
49
|
+
this.grpcFirstErrorLogged = false;
|
|
50
|
+
// Generate a unique run ID for this test run
|
|
51
|
+
this.runId = "";
|
|
52
|
+
this.runStartTime = new Date();
|
|
53
|
+
this.rootSuite = null;
|
|
54
|
+
// Track reported suites to ensure we only report each suite once and can reference correct IDs
|
|
55
|
+
this.reportedSuites = new Map();
|
|
56
|
+
this.grpcAddress =
|
|
57
|
+
options.grpcAddress ||
|
|
58
|
+
process.env.STANTERPRISE_GRPC_ADDRESS ||
|
|
59
|
+
"localhost:50051";
|
|
60
|
+
this.grpcEnabled =
|
|
61
|
+
options.grpcEnabled ??
|
|
62
|
+
(process.env.STANTERPRISE_GRPC_ENABLED || "true").toLowerCase() !==
|
|
63
|
+
"false";
|
|
64
|
+
this.grpcTimeout = options.grpcTimeout || 1000;
|
|
65
|
+
this.verbose = options.verbose || false;
|
|
66
|
+
if (this.verbose) {
|
|
67
|
+
console.log("Stanterprise Reporter: Initialized with options:", {
|
|
68
|
+
grpcAddress: this.grpcAddress,
|
|
69
|
+
grpcEnabled: this.grpcEnabled,
|
|
70
|
+
grpcTimeout: this.grpcTimeout,
|
|
71
|
+
verbose: this.verbose,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
onBegin(config, suite) {
|
|
76
|
+
// Generate a UUID for runId
|
|
77
|
+
this.runId = (0, crypto_1.randomUUID)();
|
|
78
|
+
this.rootSuite = suite;
|
|
79
|
+
// Lazily create the client if enabled.
|
|
80
|
+
if (this.grpcEnabled) {
|
|
81
|
+
try {
|
|
82
|
+
this.grpcClient = new grpc.Client(this.grpcAddress, grpc.credentials.createInsecure());
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
this.logGrpcErrorOnce("Failed to create gRPC client", e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (this.verbose) {
|
|
90
|
+
console.log("Stanterprise Reporter: gRPC disabled via STANTERPRISE_GRPC_ENABLED=false");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (this.verbose) {
|
|
94
|
+
console.log(`Stanterprise Reporter: Test run started with ID: ${this.runId}`);
|
|
95
|
+
console.log(`Number of tests: ${suite.allTests().length}`);
|
|
96
|
+
console.log(`Run started at: ${this.runStartTime.toISOString()}`);
|
|
97
|
+
}
|
|
98
|
+
// Report root suite begin and track its ID
|
|
99
|
+
const rootSuiteId = this.getSuiteId(suite);
|
|
100
|
+
// Report the suite begin event
|
|
101
|
+
this.reportSuiteBegin(suite, rootSuiteId);
|
|
102
|
+
}
|
|
103
|
+
async onExit() {
|
|
104
|
+
console.log(`Stanterprise Reporter: Test run completed - Run ID: ${this.runId}`);
|
|
105
|
+
// Cleanup gRPC client
|
|
106
|
+
try {
|
|
107
|
+
this.grpcClient?.close();
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.error("Stanterprise Reporter: Error during gRPC client cleanup in onExit:", e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Getter to access the current run ID
|
|
114
|
+
getRunId() {
|
|
115
|
+
return this.runId;
|
|
116
|
+
}
|
|
117
|
+
// Getter to access the run start time
|
|
118
|
+
getRunStartTime() {
|
|
119
|
+
return this.runStartTime;
|
|
120
|
+
}
|
|
121
|
+
onEnd(result) {
|
|
122
|
+
const runDuration = Date.now() - this.runStartTime.getTime();
|
|
123
|
+
console.log(`Stanterprise Reporter: Test run ended - Run ID: ${this.runId}`);
|
|
124
|
+
console.log(`Final result: ${result.status}`);
|
|
125
|
+
console.log(`Run duration: ${runDuration}ms (Playwright duration: ${result.duration}ms)`);
|
|
126
|
+
console.log(`Run start time: ${this.runStartTime.toISOString()}`);
|
|
127
|
+
console.log(`Playwright start time: ${result.startTime.toISOString()}`);
|
|
128
|
+
// Report root suite end
|
|
129
|
+
if (this.rootSuite) {
|
|
130
|
+
this.reportSuiteEnd(this.rootSuite, result);
|
|
131
|
+
}
|
|
132
|
+
return Promise.resolve();
|
|
133
|
+
}
|
|
134
|
+
onTestBegin(test, result) {
|
|
135
|
+
// Create unique test execution ID combining run ID and test ID
|
|
136
|
+
const uniqueTestExecutionId = `${this.runId}-${test.id}`;
|
|
137
|
+
console.log(`Stanterprise Reporter: Test started - ${test.title}`);
|
|
138
|
+
console.log(` Run ID: ${this.runId}`);
|
|
139
|
+
console.log(` Test ID (static): ${test.id}`);
|
|
140
|
+
console.log(` Unique Execution ID: ${uniqueTestExecutionId}`);
|
|
141
|
+
// Get test suite run ID from parent suite if available, ensuring suite is reported
|
|
142
|
+
const testSuiteRunId = test.parent ? this.getSuiteId(test.parent) : "";
|
|
143
|
+
// Build metadata from test annotations
|
|
144
|
+
const metadata = new Map();
|
|
145
|
+
test.annotations.forEach((annotation, index) => {
|
|
146
|
+
metadata.set(`annotation_${index}_type`, annotation.type);
|
|
147
|
+
if (annotation.description) {
|
|
148
|
+
metadata.set(`annotation_${index}_description`, annotation.description);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// Build and send the TestBegin event via generic unary call.
|
|
152
|
+
const request = new EventsNS.TestBeginEventRequest({
|
|
153
|
+
test_case: new TestCaseEntities.TestCaseRun({
|
|
154
|
+
id: test.id,
|
|
155
|
+
name: test.title,
|
|
156
|
+
run_id: this.runId,
|
|
157
|
+
test_suite_run_id: testSuiteRunId,
|
|
158
|
+
start_time: (0, utils_1.createTimestamp)(result.startTime),
|
|
159
|
+
metadata: metadata,
|
|
160
|
+
tags: test.tags,
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
// Fire-and-forget to avoid slowing tests; log errors once.
|
|
164
|
+
this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportTestBegin", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report test begin", e));
|
|
165
|
+
}
|
|
166
|
+
async onStepBegin(test, result, step) {
|
|
167
|
+
const uniqueTestExecutionId = `${this.runId}-${test.id}`;
|
|
168
|
+
const uniqueStepId = `${uniqueTestExecutionId}-${step.title}-${step.startTime.getTime()}`;
|
|
169
|
+
console.log(`Stanterprise Reporter: Step started - ${step.title}`);
|
|
170
|
+
console.log(` Category: ${step.category}`);
|
|
171
|
+
// Build metadata from step annotations
|
|
172
|
+
const metadata = new Map();
|
|
173
|
+
metadata.set("category", step.category);
|
|
174
|
+
step.annotations.forEach((annotation, index) => {
|
|
175
|
+
metadata.set(`annotation_${index}_type`, annotation.type);
|
|
176
|
+
if (annotation.description) {
|
|
177
|
+
metadata.set(`annotation_${index}_description`, annotation.description);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// Get parent step ID if this step has a parent
|
|
181
|
+
const parentStepId = step.parent
|
|
182
|
+
? `${uniqueTestExecutionId}-${step.parent.title}-${step.parent.startTime.getTime()}`
|
|
183
|
+
: "";
|
|
184
|
+
// Build and send the StepBegin event
|
|
185
|
+
const request = new EventsNS.StepBeginEventRequest({
|
|
186
|
+
step: new TestCaseEntities.StepRun({
|
|
187
|
+
id: uniqueStepId,
|
|
188
|
+
run_id: this.runId,
|
|
189
|
+
test_case_run_id: uniqueTestExecutionId,
|
|
190
|
+
title: step.title,
|
|
191
|
+
type: step.category,
|
|
192
|
+
start_time: (0, utils_1.createTimestamp)(step.startTime),
|
|
193
|
+
location: step.location
|
|
194
|
+
? `${step.location.file}:${step.location.line}:${step.location.column}`
|
|
195
|
+
: "",
|
|
196
|
+
metadata: metadata,
|
|
197
|
+
parent_step_id: parentStepId,
|
|
198
|
+
worker_index: result.workerIndex.toString(),
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
// Fire-and-forget to avoid slowing tests
|
|
202
|
+
this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportStepBegin", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report step begin", e));
|
|
203
|
+
}
|
|
204
|
+
onStepEnd(test, result, step) {
|
|
205
|
+
const uniqueTestExecutionId = `${this.runId}-${test.id}`;
|
|
206
|
+
const uniqueStepId = `${uniqueTestExecutionId}-${step.title}-${step.startTime.getTime()}`;
|
|
207
|
+
console.log(`Stanterprise Reporter: Step ended - ${step.title}`);
|
|
208
|
+
console.log(` Duration: ${step.duration}ms`);
|
|
209
|
+
// Map step error to status
|
|
210
|
+
const stepStatus = (0, utils_1.mapStepStatus)(!!step.error);
|
|
211
|
+
// Build metadata from step annotations
|
|
212
|
+
const metadata = new Map();
|
|
213
|
+
metadata.set("category", step.category);
|
|
214
|
+
step.annotations.forEach((annotation, index) => {
|
|
215
|
+
metadata.set(`annotation_${index}_type`, annotation.type);
|
|
216
|
+
if (annotation.description) {
|
|
217
|
+
metadata.set(`annotation_${index}_description`, annotation.description);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
// Get parent step ID if this step has a parent
|
|
221
|
+
const parentStepId = step.parent
|
|
222
|
+
? `${uniqueTestExecutionId}-${step.parent.title}-${step.parent.startTime.getTime()}`
|
|
223
|
+
: "";
|
|
224
|
+
// Build and send the StepEnd event
|
|
225
|
+
const request = new EventsNS.StepEndEventRequest({
|
|
226
|
+
step: new TestCaseEntities.StepRun({
|
|
227
|
+
id: uniqueStepId,
|
|
228
|
+
run_id: this.runId,
|
|
229
|
+
test_case_run_id: uniqueTestExecutionId,
|
|
230
|
+
title: step.title,
|
|
231
|
+
type: step.category,
|
|
232
|
+
start_time: (0, utils_1.createTimestamp)(step.startTime),
|
|
233
|
+
duration: (0, utils_1.createDuration)(step.duration),
|
|
234
|
+
status: stepStatus,
|
|
235
|
+
error: step.error ? step.error.message || "" : "",
|
|
236
|
+
errors: step.error ? [step.error.message || ""] : [],
|
|
237
|
+
location: step.location
|
|
238
|
+
? `${step.location.file}:${step.location.line}:${step.location.column}`
|
|
239
|
+
: "",
|
|
240
|
+
metadata: metadata,
|
|
241
|
+
parent_step_id: parentStepId,
|
|
242
|
+
worker_index: result.workerIndex.toString(),
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
// Fire-and-forget to avoid slowing tests
|
|
246
|
+
this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportStepEnd", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report step end", e));
|
|
247
|
+
}
|
|
248
|
+
onTestEnd(test, result) {
|
|
249
|
+
const uniqueTestExecutionId = `${this.runId}-${test.id}`;
|
|
250
|
+
console.log(`Stanterprise Reporter: Test ended - ${test.title}`);
|
|
251
|
+
console.log(` Status: ${result.status}`);
|
|
252
|
+
console.log(` Duration: ${result.duration}ms`);
|
|
253
|
+
// Map Playwright test status to protobuf TestStatus
|
|
254
|
+
const testStatus = (0, utils_1.mapTestStatus)(result.status);
|
|
255
|
+
// Process attachments (screenshots, videos, etc.)
|
|
256
|
+
const attachments = (0, utils_1.processAttachments)(result);
|
|
257
|
+
// Extract error information if the test failed
|
|
258
|
+
const { errorMessage, stackTrace, errors } = (0, utils_1.extractErrorInfo)(result);
|
|
259
|
+
// Get test suite run ID from parent suite if available, ensuring suite is reported
|
|
260
|
+
const testSuiteRunId = test.parent ? this.getSuiteId(test.parent) : "";
|
|
261
|
+
// Build metadata from test annotations and result metadata
|
|
262
|
+
const metadata = new Map();
|
|
263
|
+
test.annotations.forEach((annotation, index) => {
|
|
264
|
+
metadata.set(`annotation_${index}_type`, annotation.type);
|
|
265
|
+
if (annotation.description) {
|
|
266
|
+
metadata.set(`annotation_${index}_description`, annotation.description);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
result.annotations.forEach((annotation, index) => {
|
|
270
|
+
metadata.set(`result_annotation_${index}_type`, annotation.type);
|
|
271
|
+
if (annotation.description) {
|
|
272
|
+
metadata.set(`result_annotation_${index}_description`, annotation.description);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// Build and send the TestEnd event
|
|
276
|
+
const request = new EventsNS.TestEndEventRequest({
|
|
277
|
+
test_case: new TestCaseEntities.TestCaseRun({
|
|
278
|
+
id: test.id,
|
|
279
|
+
name: test.title,
|
|
280
|
+
run_id: this.runId,
|
|
281
|
+
test_suite_run_id: testSuiteRunId,
|
|
282
|
+
status: testStatus,
|
|
283
|
+
start_time: (0, utils_1.createTimestamp)(result.startTime),
|
|
284
|
+
attachments: attachments,
|
|
285
|
+
error_message: errorMessage,
|
|
286
|
+
stack_trace: stackTrace,
|
|
287
|
+
errors: errors,
|
|
288
|
+
metadata: metadata,
|
|
289
|
+
tags: test.tags,
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
// Fire-and-forget to avoid slowing tests
|
|
293
|
+
this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportTestEnd", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report test end", e));
|
|
294
|
+
}
|
|
295
|
+
onTestFail(test, result) {
|
|
296
|
+
const uniqueTestExecutionId = `${this.runId}-${test.id}`;
|
|
297
|
+
if (this.verbose) {
|
|
298
|
+
console.log(`Stanterprise Reporter: Test failed - ${test.title}`);
|
|
299
|
+
}
|
|
300
|
+
// Extract failure details
|
|
301
|
+
const { errorMessage: failureMessage, stackTrace } = (0, utils_1.extractErrorInfo)(result);
|
|
302
|
+
// Process attachments for failed tests
|
|
303
|
+
const attachments = (0, utils_1.processAttachments)(result);
|
|
304
|
+
// Build and send the TestFailure event
|
|
305
|
+
const request = new EventsNS.TestFailureEventRequest({
|
|
306
|
+
test_id: uniqueTestExecutionId,
|
|
307
|
+
failure_message: failureMessage,
|
|
308
|
+
stack_trace: stackTrace,
|
|
309
|
+
timestamp: (0, utils_1.createTimestampFromMs)(Date.now()),
|
|
310
|
+
attachments: attachments,
|
|
311
|
+
});
|
|
312
|
+
// Fire-and-forget to avoid slowing tests
|
|
313
|
+
this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportTestFailure", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report test failure", e));
|
|
314
|
+
}
|
|
315
|
+
onError(error) {
|
|
316
|
+
console.error("Stanterprise Reporter: An error occurred during the test run");
|
|
317
|
+
console.error(`Error: ${error.message}`);
|
|
318
|
+
if (error.stack) {
|
|
319
|
+
console.error(`Stack trace: ${error.stack}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
onStdErr(chunk, test, result) {
|
|
323
|
+
console.error(`Stanterprise Reporter: Standard error output - ${chunk.toString()}`);
|
|
324
|
+
}
|
|
325
|
+
onStdOut(chunk, test, result) {
|
|
326
|
+
// console.log(`Stanterprise Reporter: Standard output - ${chunk.toString()}`);
|
|
327
|
+
}
|
|
328
|
+
// Helper: Get suite ID for a suite, ensuring it's been reported
|
|
329
|
+
getSuiteId(suite) {
|
|
330
|
+
// Check if we've already reported this suite
|
|
331
|
+
if (this.reportedSuites.has(suite)) {
|
|
332
|
+
return this.reportedSuites.get(suite);
|
|
333
|
+
}
|
|
334
|
+
// Generate a unique suite ID based on suite hierarchy
|
|
335
|
+
const titlePath = suite.titlePath().join("/") || "root";
|
|
336
|
+
const suiteId = `${this.runId}-suite-${titlePath}`;
|
|
337
|
+
// Track that we've reported this suite
|
|
338
|
+
this.reportedSuites.set(suite, suiteId);
|
|
339
|
+
return suiteId;
|
|
340
|
+
}
|
|
341
|
+
// Helper: Report suite begin event
|
|
342
|
+
reportSuiteBegin(suite, suiteId) {
|
|
343
|
+
const id = suiteId || `${this.runId}-suite-${suite.title || "root"}`;
|
|
344
|
+
console.log(`Stanterprise Reporter: Suite started - ${suite.title || "root"}`);
|
|
345
|
+
console.log(` Suite type: ${suite.type}`);
|
|
346
|
+
// Build metadata from suite location
|
|
347
|
+
const metadata = new Map();
|
|
348
|
+
metadata.set("suite_type", suite.type);
|
|
349
|
+
if (suite.location) {
|
|
350
|
+
metadata.set("location_file", suite.location.file);
|
|
351
|
+
metadata.set("location_line", suite.location.line.toString());
|
|
352
|
+
metadata.set("location_column", suite.location.column.toString());
|
|
353
|
+
}
|
|
354
|
+
// Build and send the SuiteBegin event
|
|
355
|
+
const request = new EventsNS.SuiteBeginEventRequest({
|
|
356
|
+
suite: new TestSuiteEntities.TestSuiteRun({
|
|
357
|
+
id: id,
|
|
358
|
+
name: suite.title || "root",
|
|
359
|
+
start_time: (0, utils_1.createTimestamp)(this.runStartTime),
|
|
360
|
+
metadata: metadata,
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
// Fire-and-forget to avoid slowing tests
|
|
364
|
+
this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportSuiteBegin", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report suite begin", e));
|
|
365
|
+
}
|
|
366
|
+
// Helper: Report suite end event
|
|
367
|
+
reportSuiteEnd(suite, result) {
|
|
368
|
+
const suiteId = `${this.runId}-suite-${suite.title || "root"}`;
|
|
369
|
+
const endTime = new Date();
|
|
370
|
+
const duration = endTime.getTime() - this.runStartTime.getTime();
|
|
371
|
+
console.log(`Stanterprise Reporter: Suite ended - ${suite.title || "root"}`);
|
|
372
|
+
console.log(` Duration: ${duration}ms`);
|
|
373
|
+
// Map result status to protobuf TestStatus
|
|
374
|
+
const suiteStatus = this.mapSuiteStatus(result.status);
|
|
375
|
+
// Build metadata from suite location
|
|
376
|
+
const metadata = new Map();
|
|
377
|
+
metadata.set("suite_type", suite.type);
|
|
378
|
+
metadata.set("final_result", result.status);
|
|
379
|
+
if (suite.location) {
|
|
380
|
+
metadata.set("location_file", suite.location.file);
|
|
381
|
+
metadata.set("location_line", suite.location.line.toString());
|
|
382
|
+
metadata.set("location_column", suite.location.column.toString());
|
|
383
|
+
}
|
|
384
|
+
// Build and send the SuiteEnd event
|
|
385
|
+
const request = new EventsNS.SuiteEndEventRequest({
|
|
386
|
+
suite: new TestSuiteEntities.TestSuiteRun({
|
|
387
|
+
id: suiteId,
|
|
388
|
+
name: suite.title || "root",
|
|
389
|
+
start_time: (0, utils_1.createTimestamp)(this.runStartTime),
|
|
390
|
+
end_time: (0, utils_1.createTimestamp)(endTime),
|
|
391
|
+
duration: (0, utils_1.createDuration)(duration),
|
|
392
|
+
status: suiteStatus,
|
|
393
|
+
metadata: metadata,
|
|
394
|
+
}),
|
|
395
|
+
});
|
|
396
|
+
// Fire-and-forget to avoid slowing tests
|
|
397
|
+
this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportSuiteEnd", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report suite end", e));
|
|
398
|
+
}
|
|
399
|
+
// Helper: Map FullResult status to protobuf TestStatus
|
|
400
|
+
mapSuiteStatus(status) {
|
|
401
|
+
switch (status) {
|
|
402
|
+
case "passed":
|
|
403
|
+
return TestStatus.PASSED;
|
|
404
|
+
case "failed":
|
|
405
|
+
return TestStatus.FAILED;
|
|
406
|
+
case "timedout":
|
|
407
|
+
return TestStatus.FAILED;
|
|
408
|
+
case "interrupted":
|
|
409
|
+
return TestStatus.BROKEN;
|
|
410
|
+
default:
|
|
411
|
+
return TestStatus.UNKNOWN;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Helper: log first gRPC error once and disable further attempts
|
|
415
|
+
logGrpcErrorOnce(prefix, err) {
|
|
416
|
+
if (this.grpcFirstErrorLogged)
|
|
417
|
+
return;
|
|
418
|
+
this.grpcFirstErrorLogged = true;
|
|
419
|
+
const details = err instanceof Error ? `${err.message}` : String(err);
|
|
420
|
+
console.warn(`${prefix}. gRPC disabled for the remainder of the run. Address=${this.grpcAddress}. Details: ${details}`);
|
|
421
|
+
this.grpcEnabled = false;
|
|
422
|
+
}
|
|
423
|
+
// Helper: generic unary call using raw method path
|
|
424
|
+
async reportUnary(path, message, deadlineMs = 1000) {
|
|
425
|
+
if (!this.grpcEnabled || !this.grpcClient) {
|
|
426
|
+
return Buffer.alloc(0);
|
|
427
|
+
}
|
|
428
|
+
const reqSerialize = (arg) => {
|
|
429
|
+
const m = arg;
|
|
430
|
+
const bytes = m?.serializeBinary
|
|
431
|
+
? m.serializeBinary()
|
|
432
|
+
: m?.serialize
|
|
433
|
+
? m.serialize()
|
|
434
|
+
: new Uint8Array(0);
|
|
435
|
+
return Buffer.from(bytes);
|
|
436
|
+
};
|
|
437
|
+
const resDeserialize = (bytes) => bytes;
|
|
438
|
+
const metadata = new grpc.Metadata();
|
|
439
|
+
const callOptions = {
|
|
440
|
+
deadline: new Date(Date.now() + deadlineMs),
|
|
441
|
+
};
|
|
442
|
+
return new Promise((resolve, reject) => {
|
|
443
|
+
try {
|
|
444
|
+
this.grpcClient.makeUnaryRequest(path, reqSerialize, resDeserialize, message, metadata, callOptions, (err, response) => {
|
|
445
|
+
if (err)
|
|
446
|
+
return reject(err);
|
|
447
|
+
resolve(response);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
reject(e);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
exports.default = StanterpriseReporter;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Stanterprise Playwright Reporter
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the Stanterprise Reporter
|
|
6
|
+
*/
|
|
7
|
+
export interface StanterpriseReporterOptions {
|
|
8
|
+
/**
|
|
9
|
+
* gRPC server address
|
|
10
|
+
* @default "localhost:50051" or process.env.STANTERPRISE_GRPC_ADDRESS
|
|
11
|
+
*/
|
|
12
|
+
grpcAddress?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Whether gRPC reporting is enabled
|
|
15
|
+
* @default true or process.env.STANTERPRISE_GRPC_ENABLED !== "false"
|
|
16
|
+
*/
|
|
17
|
+
grpcEnabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Timeout for gRPC calls in milliseconds
|
|
20
|
+
* @default 1000
|
|
21
|
+
*/
|
|
22
|
+
grpcTimeout?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to include verbose logging
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
verbose?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Internal type for managing test execution state
|
|
31
|
+
*/
|
|
32
|
+
export interface TestExecutionContext {
|
|
33
|
+
/**
|
|
34
|
+
* Unique ID combining run ID and test ID
|
|
35
|
+
*/
|
|
36
|
+
uniqueTestExecutionId: string;
|
|
37
|
+
/**
|
|
38
|
+
* The global run ID for this test execution
|
|
39
|
+
*/
|
|
40
|
+
runId: string;
|
|
41
|
+
/**
|
|
42
|
+
* The test ID from Playwright
|
|
43
|
+
*/
|
|
44
|
+
testId: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Internal type for managing step execution state
|
|
48
|
+
*/
|
|
49
|
+
export interface StepExecutionContext extends TestExecutionContext {
|
|
50
|
+
/**
|
|
51
|
+
* Unique ID for the step
|
|
52
|
+
*/
|
|
53
|
+
uniqueStepId: string;
|
|
54
|
+
/**
|
|
55
|
+
* Step title
|
|
56
|
+
*/
|
|
57
|
+
stepTitle: string;
|
|
58
|
+
/**
|
|
59
|
+
* Step start time
|
|
60
|
+
*/
|
|
61
|
+
stepStartTime: Date;
|
|
62
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for processing test attachments
|
|
3
|
+
*/
|
|
4
|
+
import type { TestResult } from "@playwright/test/reporter";
|
|
5
|
+
import { common } from "@stanterprise/protobuf";
|
|
6
|
+
declare const Attachment: typeof common.v1.common.Attachment;
|
|
7
|
+
/**
|
|
8
|
+
* Process Playwright test attachments into protobuf Attachment objects
|
|
9
|
+
*/
|
|
10
|
+
export declare function processAttachments(result: TestResult): InstanceType<typeof Attachment>[];
|
|
11
|
+
/**
|
|
12
|
+
* Extract error information from test results
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractErrorInfo(result: TestResult): {
|
|
15
|
+
errorMessage: string;
|
|
16
|
+
stackTrace: string;
|
|
17
|
+
errors: string[];
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processAttachments = processAttachments;
|
|
4
|
+
exports.extractErrorInfo = extractErrorInfo;
|
|
5
|
+
const protobuf_1 = require("@stanterprise/protobuf");
|
|
6
|
+
const Attachment = protobuf_1.common.v1.common.Attachment;
|
|
7
|
+
/**
|
|
8
|
+
* Process Playwright test attachments into protobuf Attachment objects
|
|
9
|
+
*/
|
|
10
|
+
function processAttachments(result) {
|
|
11
|
+
const attachments = [];
|
|
12
|
+
if (!result.attachments || result.attachments.length === 0) {
|
|
13
|
+
return attachments;
|
|
14
|
+
}
|
|
15
|
+
for (const attachment of result.attachments) {
|
|
16
|
+
const att = new Attachment({
|
|
17
|
+
name: attachment.name,
|
|
18
|
+
mime_type: attachment.contentType,
|
|
19
|
+
});
|
|
20
|
+
// Use path as URI if available, otherwise use the body content
|
|
21
|
+
if (attachment.path) {
|
|
22
|
+
att.uri = attachment.path;
|
|
23
|
+
}
|
|
24
|
+
else if (attachment.body) {
|
|
25
|
+
att.content = attachment.body;
|
|
26
|
+
}
|
|
27
|
+
attachments.push(att);
|
|
28
|
+
}
|
|
29
|
+
return attachments;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extract error information from test results
|
|
33
|
+
*/
|
|
34
|
+
function extractErrorInfo(result) {
|
|
35
|
+
let errorMessage = "";
|
|
36
|
+
let stackTrace = "";
|
|
37
|
+
const errors = [];
|
|
38
|
+
if (result.errors && result.errors.length > 0) {
|
|
39
|
+
errorMessage = result.errors.map((e) => e.message || "").join("\n");
|
|
40
|
+
stackTrace = result.errors.map((e) => e.stack || "").join("\n");
|
|
41
|
+
errors.push(...result.errors.map((e) => e.message || ""));
|
|
42
|
+
}
|
|
43
|
+
return { errorMessage, stackTrace, errors };
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* Utility exports - centralized access to all utility functions
|
|
19
|
+
*/
|
|
20
|
+
__exportStar(require("./statusMapper"), exports);
|
|
21
|
+
__exportStar(require("./attachmentProcessor"), exports);
|
|
22
|
+
__exportStar(require("./timeHelpers"), exports);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for mapping Playwright types to protobuf types
|
|
3
|
+
*/
|
|
4
|
+
import type { TestResult } from "@playwright/test/reporter";
|
|
5
|
+
/**
|
|
6
|
+
* Map Playwright test status to protobuf TestStatus
|
|
7
|
+
*/
|
|
8
|
+
export declare function mapTestStatus(status: TestResult["status"]): number;
|
|
9
|
+
/**
|
|
10
|
+
* Map step error to test status
|
|
11
|
+
*/
|
|
12
|
+
export declare function mapStepStatus(hasError: boolean): number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapTestStatus = mapTestStatus;
|
|
4
|
+
exports.mapStepStatus = mapStepStatus;
|
|
5
|
+
const protobuf_1 = require("@stanterprise/protobuf");
|
|
6
|
+
const TestStatus = protobuf_1.common.v1.common.TestStatus;
|
|
7
|
+
/**
|
|
8
|
+
* Map Playwright test status to protobuf TestStatus
|
|
9
|
+
*/
|
|
10
|
+
function mapTestStatus(status) {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case "passed":
|
|
13
|
+
return TestStatus.PASSED;
|
|
14
|
+
case "failed":
|
|
15
|
+
return TestStatus.FAILED;
|
|
16
|
+
case "skipped":
|
|
17
|
+
return TestStatus.SKIPPED;
|
|
18
|
+
case "timedOut":
|
|
19
|
+
return TestStatus.FAILED; // Treat timeout as failed
|
|
20
|
+
default:
|
|
21
|
+
return TestStatus.UNKNOWN;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Map step error to test status
|
|
26
|
+
*/
|
|
27
|
+
function mapStepStatus(hasError) {
|
|
28
|
+
return hasError ? TestStatus.FAILED : TestStatus.PASSED;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for creating timestamps and durations
|
|
3
|
+
*/
|
|
4
|
+
import { google } from "@stanterprise/protobuf";
|
|
5
|
+
declare const Timestamp: typeof google.google.protobuf.Timestamp;
|
|
6
|
+
/**
|
|
7
|
+
* Create a protobuf Timestamp from a JavaScript Date
|
|
8
|
+
*/
|
|
9
|
+
export declare function createTimestamp(date: Date): InstanceType<typeof Timestamp>;
|
|
10
|
+
/**
|
|
11
|
+
* Create a protobuf Timestamp from milliseconds since epoch
|
|
12
|
+
*/
|
|
13
|
+
export declare function createTimestampFromMs(ms: number): InstanceType<typeof Timestamp>;
|
|
14
|
+
/**
|
|
15
|
+
* Create a duration object from milliseconds.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: google.protobuf.Duration is defined in the @stanterprise/protobuf schema
|
|
18
|
+
* but not exported in the package's main barrel export. Since Duration and Timestamp
|
|
19
|
+
* have identical structure (seconds: number, nanos: number) and the protobuf serialization
|
|
20
|
+
* is wire-compatible, we use Timestamp as a Duration substitute. This works correctly at runtime.
|
|
21
|
+
*
|
|
22
|
+
* @param durationMs Duration in milliseconds
|
|
23
|
+
* @returns A Duration object (implemented as Timestamp due to export limitations)
|
|
24
|
+
*/
|
|
25
|
+
export declare function createDuration(durationMs: number): any;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTimestamp = createTimestamp;
|
|
4
|
+
exports.createTimestampFromMs = createTimestampFromMs;
|
|
5
|
+
exports.createDuration = createDuration;
|
|
6
|
+
/**
|
|
7
|
+
* Utility functions for creating timestamps and durations
|
|
8
|
+
*/
|
|
9
|
+
const protobuf_1 = require("@stanterprise/protobuf");
|
|
10
|
+
const Timestamp = protobuf_1.google.google.protobuf.Timestamp;
|
|
11
|
+
/**
|
|
12
|
+
* Create a protobuf Timestamp from a JavaScript Date
|
|
13
|
+
*/
|
|
14
|
+
function createTimestamp(date) {
|
|
15
|
+
return new Timestamp({
|
|
16
|
+
seconds: Math.floor(date.getTime() / 1000),
|
|
17
|
+
nanos: (date.getTime() % 1000) * 1000000,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a protobuf Timestamp from milliseconds since epoch
|
|
22
|
+
*/
|
|
23
|
+
function createTimestampFromMs(ms) {
|
|
24
|
+
return new Timestamp({
|
|
25
|
+
seconds: Math.floor(ms / 1000),
|
|
26
|
+
nanos: (ms % 1000) * 1000000,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a duration object from milliseconds.
|
|
31
|
+
*
|
|
32
|
+
* NOTE: google.protobuf.Duration is defined in the @stanterprise/protobuf schema
|
|
33
|
+
* but not exported in the package's main barrel export. Since Duration and Timestamp
|
|
34
|
+
* have identical structure (seconds: number, nanos: number) and the protobuf serialization
|
|
35
|
+
* is wire-compatible, we use Timestamp as a Duration substitute. This works correctly at runtime.
|
|
36
|
+
*
|
|
37
|
+
* @param durationMs Duration in milliseconds
|
|
38
|
+
* @returns A Duration object (implemented as Timestamp due to export limitations)
|
|
39
|
+
*/
|
|
40
|
+
function createDuration(durationMs) {
|
|
41
|
+
return new Timestamp({
|
|
42
|
+
seconds: Math.floor(durationMs / 1000),
|
|
43
|
+
nanos: (durationMs % 1000) * 1000000,
|
|
44
|
+
});
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stanterprise/playwright-reporter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Custom Playwright test reporter that sends test results to Stanterprise via gRPC",
|
|
5
|
+
"homepage": "https://github.com/stanterprise/stanterprise-playwright-reporter#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/stanterprise/stanterprise-playwright-reporter/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/stanterprise/stanterprise-playwright-reporter.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "Stanterprise Team",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/**/*",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"playwright",
|
|
24
|
+
"reporter",
|
|
25
|
+
"test",
|
|
26
|
+
"testing",
|
|
27
|
+
"stanterprise",
|
|
28
|
+
"grpc",
|
|
29
|
+
"playwright-reporter"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@playwright/test": ">=1.20.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
41
|
+
"test": "jest",
|
|
42
|
+
"test:watch": "jest --watch",
|
|
43
|
+
"test:coverage": "jest --coverage"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@playwright/test": "^1.52.0",
|
|
47
|
+
"@types/jest": "^29.5.14",
|
|
48
|
+
"@types/node": "^22.15.29",
|
|
49
|
+
"jest": "^29.7.0",
|
|
50
|
+
"ts-jest": "^29.4.5",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@grpc/grpc-js": "^1.14.1",
|
|
55
|
+
"@protobuf-ts/grpc-transport": "^2.11.1",
|
|
56
|
+
"@protobuf-ts/grpcweb-transport": "^2.11.1",
|
|
57
|
+
"@stanterprise/protobuf": "^0.0.11"
|
|
58
|
+
}
|
|
59
|
+
}
|