@stanterprise/playwright-reporter 1.0.0

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 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
+
@@ -0,0 +1,3 @@
1
+ export { default } from "./reporter";
2
+ export { default as StanterpriseReporter } from "./reporter";
3
+ export type { StanterpriseReporterOptions, TestExecutionContext, StepExecutionContext } from "./types";
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
+ }
@@ -0,0 +1,449 @@
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
+ class StanterpriseReporter {
41
+ constructor(options = {}) {
42
+ // Generic gRPC client (we call unary methods by path directly).
43
+ this.grpcClient = null;
44
+ this.grpcFirstErrorLogged = false;
45
+ // Generate a unique run ID for this test run
46
+ this.runId = "";
47
+ this.runStartTime = new Date();
48
+ this.rootSuite = null;
49
+ // Track reported suites to ensure we only report each suite once and can reference correct IDs
50
+ this.reportedSuites = new Map();
51
+ this.grpcAddress =
52
+ options.grpcAddress ||
53
+ process.env.STANTERPRISE_GRPC_ADDRESS ||
54
+ "localhost:50051";
55
+ this.grpcEnabled =
56
+ options.grpcEnabled ??
57
+ (process.env.STANTERPRISE_GRPC_ENABLED || "true").toLowerCase() !==
58
+ "false";
59
+ this.grpcTimeout = options.grpcTimeout || 1000;
60
+ this.verbose = options.verbose || false;
61
+ if (this.verbose) {
62
+ console.log("Stanterprise Reporter: Initialized with options:", {
63
+ grpcAddress: this.grpcAddress,
64
+ grpcEnabled: this.grpcEnabled,
65
+ grpcTimeout: this.grpcTimeout,
66
+ verbose: this.verbose,
67
+ });
68
+ }
69
+ }
70
+ onBegin(config, suite) {
71
+ // Generate a UUID for runId
72
+ this.runId = (0, crypto_1.randomUUID)();
73
+ this.rootSuite = suite;
74
+ // Lazily create the client if enabled.
75
+ if (this.grpcEnabled) {
76
+ try {
77
+ this.grpcClient = new grpc.Client(this.grpcAddress, grpc.credentials.createInsecure());
78
+ }
79
+ catch (e) {
80
+ this.logGrpcErrorOnce("Failed to create gRPC client", e);
81
+ }
82
+ }
83
+ else {
84
+ console.log("Stanterprise Reporter: gRPC disabled via STANTERPRISE_GRPC_ENABLED=false");
85
+ }
86
+ console.log(`Stanterprise Reporter: Test run started with ID: ${this.runId}`);
87
+ console.log(`Number of tests: ${suite.allTests().length}`);
88
+ console.log(`Run started at: ${this.runStartTime.toISOString()}`);
89
+ // Report root suite begin and track its ID
90
+ const rootSuiteId = this.getSuiteId(suite);
91
+ }
92
+ async onExit() {
93
+ console.log(`Stanterprise Reporter: Test run completed - Run ID: ${this.runId}`);
94
+ // Cleanup gRPC client
95
+ try {
96
+ this.grpcClient?.close();
97
+ }
98
+ catch (e) {
99
+ console.error("Stanterprise Reporter: Error during gRPC client cleanup in onExit:", e);
100
+ }
101
+ }
102
+ // Getter to access the current run ID
103
+ getRunId() {
104
+ return this.runId;
105
+ }
106
+ // Getter to access the run start time
107
+ getRunStartTime() {
108
+ return this.runStartTime;
109
+ }
110
+ onEnd(result) {
111
+ const runDuration = Date.now() - this.runStartTime.getTime();
112
+ console.log(`Stanterprise Reporter: Test run ended - Run ID: ${this.runId}`);
113
+ console.log(`Final result: ${result.status}`);
114
+ console.log(`Run duration: ${runDuration}ms (Playwright duration: ${result.duration}ms)`);
115
+ console.log(`Run start time: ${this.runStartTime.toISOString()}`);
116
+ console.log(`Playwright start time: ${result.startTime.toISOString()}`);
117
+ // Report root suite end
118
+ if (this.rootSuite) {
119
+ this.reportSuiteEnd(this.rootSuite, result);
120
+ }
121
+ return Promise.resolve();
122
+ }
123
+ onTestBegin(test, result) {
124
+ // Create unique test execution ID combining run ID and test ID
125
+ const uniqueTestExecutionId = `${this.runId}-${test.id}`;
126
+ console.log(`Stanterprise Reporter: Test started - ${test.title}`);
127
+ console.log(` Run ID: ${this.runId}`);
128
+ console.log(` Test ID (static): ${test.id}`);
129
+ console.log(` Unique Execution ID: ${uniqueTestExecutionId}`);
130
+ // Get test suite run ID from parent suite if available, ensuring suite is reported
131
+ const testSuiteRunId = test.parent ? this.getSuiteId(test.parent) : "";
132
+ // Build metadata from test annotations
133
+ const metadata = new Map();
134
+ test.annotations.forEach((annotation, index) => {
135
+ metadata.set(`annotation_${index}_type`, annotation.type);
136
+ if (annotation.description) {
137
+ metadata.set(`annotation_${index}_description`, annotation.description);
138
+ }
139
+ });
140
+ // Build and send the TestBegin event via generic unary call.
141
+ const request = new protobuf_1.events.TestBeginEventRequest({
142
+ test_case: new protobuf_1.entities.TestCaseRun({
143
+ id: uniqueTestExecutionId,
144
+ title: test.title,
145
+ run_id: this.runId,
146
+ test_id: test.id,
147
+ test_suite_run_id: testSuiteRunId,
148
+ start_time: (0, utils_1.createTimestamp)(result.startTime),
149
+ metadata: metadata,
150
+ actual_tags: test.tags,
151
+ }),
152
+ });
153
+ // Fire-and-forget to avoid slowing tests; log errors once.
154
+ this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportTestBegin", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report test begin", e));
155
+ }
156
+ async onStepBegin(test, result, step) {
157
+ const uniqueTestExecutionId = `${this.runId}-${test.id}`;
158
+ const uniqueStepId = `${uniqueTestExecutionId}-${step.title}-${step.startTime.getTime()}`;
159
+ console.log(`Stanterprise Reporter: Step started - ${step.title}`);
160
+ console.log(` Category: ${step.category}`);
161
+ // Build metadata from step annotations
162
+ const metadata = new Map();
163
+ metadata.set("category", step.category);
164
+ step.annotations.forEach((annotation, index) => {
165
+ metadata.set(`annotation_${index}_type`, annotation.type);
166
+ if (annotation.description) {
167
+ metadata.set(`annotation_${index}_description`, annotation.description);
168
+ }
169
+ });
170
+ // Get parent step ID if this step has a parent
171
+ const parentStepId = step.parent
172
+ ? `${uniqueTestExecutionId}-${step.parent.title}-${step.parent.startTime.getTime()}`
173
+ : "";
174
+ // Build and send the StepBegin event
175
+ const request = new protobuf_1.events.StepBeginEventRequest({
176
+ step: new protobuf_1.entities.StepRun({
177
+ id: uniqueStepId,
178
+ run_id: this.runId,
179
+ test_case_run_id: uniqueTestExecutionId,
180
+ title: step.title,
181
+ type: step.category,
182
+ start_time: (0, utils_1.createTimestamp)(step.startTime),
183
+ location: step.location
184
+ ? `${step.location.file}:${step.location.line}:${step.location.column}`
185
+ : "",
186
+ metadata: metadata,
187
+ parent_step_id: parentStepId,
188
+ worker_index: result.workerIndex.toString(),
189
+ }),
190
+ });
191
+ // Fire-and-forget to avoid slowing tests
192
+ this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportStepBegin", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report step begin", e));
193
+ }
194
+ onStepEnd(test, result, step) {
195
+ const uniqueTestExecutionId = `${this.runId}-${test.id}`;
196
+ const uniqueStepId = `${uniqueTestExecutionId}-${step.title}-${step.startTime.getTime()}`;
197
+ console.log(`Stanterprise Reporter: Step ended - ${step.title}`);
198
+ console.log(` Duration: ${step.duration}ms`);
199
+ // Map step error to status
200
+ const stepStatus = (0, utils_1.mapStepStatus)(!!step.error);
201
+ // Build metadata from step annotations
202
+ const metadata = new Map();
203
+ metadata.set("category", step.category);
204
+ step.annotations.forEach((annotation, index) => {
205
+ metadata.set(`annotation_${index}_type`, annotation.type);
206
+ if (annotation.description) {
207
+ metadata.set(`annotation_${index}_description`, annotation.description);
208
+ }
209
+ });
210
+ // Get parent step ID if this step has a parent
211
+ const parentStepId = step.parent
212
+ ? `${uniqueTestExecutionId}-${step.parent.title}-${step.parent.startTime.getTime()}`
213
+ : "";
214
+ // Build and send the StepEnd event
215
+ const request = new protobuf_1.events.StepEndEventRequest({
216
+ step: new protobuf_1.entities.StepRun({
217
+ id: uniqueStepId,
218
+ run_id: this.runId,
219
+ test_case_run_id: uniqueTestExecutionId,
220
+ title: step.title,
221
+ type: step.category,
222
+ start_time: (0, utils_1.createTimestamp)(step.startTime),
223
+ duration: (0, utils_1.createDuration)(step.duration),
224
+ status: stepStatus,
225
+ error: step.error ? step.error.message || "" : "",
226
+ errors: step.error ? [step.error.message || ""] : [],
227
+ location: step.location
228
+ ? `${step.location.file}:${step.location.line}:${step.location.column}`
229
+ : "",
230
+ metadata: metadata,
231
+ parent_step_id: parentStepId,
232
+ worker_index: result.workerIndex.toString(),
233
+ }),
234
+ });
235
+ // Fire-and-forget to avoid slowing tests
236
+ this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportStepEnd", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report step end", e));
237
+ }
238
+ onTestEnd(test, result) {
239
+ const uniqueTestExecutionId = `${this.runId}-${test.id}`;
240
+ console.log(`Stanterprise Reporter: Test ended - ${test.title}`);
241
+ console.log(` Status: ${result.status}`);
242
+ console.log(` Duration: ${result.duration}ms`);
243
+ // Map Playwright test status to protobuf TestStatus
244
+ const testStatus = (0, utils_1.mapTestStatus)(result.status);
245
+ // Process attachments (screenshots, videos, etc.)
246
+ const attachments = (0, utils_1.processAttachments)(result);
247
+ // Extract error information if the test failed
248
+ const { errorMessage, stackTrace, errors } = (0, utils_1.extractErrorInfo)(result);
249
+ // Get test suite run ID from parent suite if available, ensuring suite is reported
250
+ const testSuiteRunId = test.parent ? this.getSuiteId(test.parent) : "";
251
+ // Build metadata from test annotations and result metadata
252
+ const metadata = new Map();
253
+ test.annotations.forEach((annotation, index) => {
254
+ metadata.set(`annotation_${index}_type`, annotation.type);
255
+ if (annotation.description) {
256
+ metadata.set(`annotation_${index}_description`, annotation.description);
257
+ }
258
+ });
259
+ result.annotations.forEach((annotation, index) => {
260
+ metadata.set(`result_annotation_${index}_type`, annotation.type);
261
+ if (annotation.description) {
262
+ metadata.set(`result_annotation_${index}_description`, annotation.description);
263
+ }
264
+ });
265
+ // Build and send the TestEnd event
266
+ const request = new protobuf_1.events.TestEndEventRequest({
267
+ test_case: new protobuf_1.entities.TestCaseRun({
268
+ id: uniqueTestExecutionId,
269
+ title: test.title,
270
+ run_id: this.runId,
271
+ test_id: test.id,
272
+ test_suite_run_id: testSuiteRunId,
273
+ status: testStatus,
274
+ start_time: (0, utils_1.createTimestamp)(result.startTime),
275
+ attachments: attachments,
276
+ error_message: errorMessage,
277
+ stack_trace: stackTrace,
278
+ errors: errors,
279
+ metadata: metadata,
280
+ actual_tags: test.tags,
281
+ }),
282
+ });
283
+ // Fire-and-forget to avoid slowing tests
284
+ this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportTestEnd", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report test end", e));
285
+ }
286
+ onTestFail(test, result) {
287
+ const uniqueTestExecutionId = `${this.runId}-${test.id}`;
288
+ if (this.verbose) {
289
+ console.log(`Stanterprise Reporter: Test failed - ${test.title}`);
290
+ }
291
+ // Extract failure details
292
+ const { errorMessage: failureMessage, stackTrace } = (0, utils_1.extractErrorInfo)(result);
293
+ // Process attachments for failed tests
294
+ const attachments = (0, utils_1.processAttachments)(result);
295
+ // Build and send the TestFailure event
296
+ const request = new protobuf_1.events.TestFailureEventRequest({
297
+ test_id: uniqueTestExecutionId,
298
+ failure_message: failureMessage,
299
+ stack_trace: stackTrace,
300
+ timestamp: (0, utils_1.createTimestampFromMs)(Date.now()),
301
+ attachments: attachments,
302
+ });
303
+ // Fire-and-forget to avoid slowing tests
304
+ this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportTestFailure", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report test failure", e));
305
+ }
306
+ onError(error) {
307
+ console.error("Stanterprise Reporter: An error occurred during the test run");
308
+ console.error(`Error: ${error.message}`);
309
+ if (error.stack) {
310
+ console.error(`Stack trace: ${error.stack}`);
311
+ }
312
+ }
313
+ onStdErr(chunk, test, result) {
314
+ console.error(`Stanterprise Reporter: Standard error output - ${chunk.toString()}`);
315
+ }
316
+ onStdOut(chunk, test, result) {
317
+ // console.log(`Stanterprise Reporter: Standard output - ${chunk.toString()}`);
318
+ }
319
+ // Helper: Get suite ID for a suite, ensuring it's been reported
320
+ getSuiteId(suite) {
321
+ // Check if we've already reported this suite
322
+ if (this.reportedSuites.has(suite)) {
323
+ return this.reportedSuites.get(suite);
324
+ }
325
+ // Generate a unique suite ID based on suite hierarchy
326
+ const titlePath = suite.titlePath().join('/') || 'root';
327
+ const suiteId = `${this.runId}-suite-${titlePath}`;
328
+ // Report the suite begin event
329
+ this.reportSuiteBegin(suite, suiteId);
330
+ // Track that we've reported this suite
331
+ this.reportedSuites.set(suite, suiteId);
332
+ return suiteId;
333
+ }
334
+ // Helper: Report suite begin event
335
+ reportSuiteBegin(suite, suiteId) {
336
+ const id = suiteId || `${this.runId}-suite-${suite.title || 'root'}`;
337
+ console.log(`Stanterprise Reporter: Suite started - ${suite.title || 'root'}`);
338
+ console.log(` Suite type: ${suite.type}`);
339
+ // Build metadata from suite location
340
+ const metadata = new Map();
341
+ metadata.set("suite_type", suite.type);
342
+ if (suite.location) {
343
+ metadata.set("location_file", suite.location.file);
344
+ metadata.set("location_line", suite.location.line.toString());
345
+ metadata.set("location_column", suite.location.column.toString());
346
+ }
347
+ // Build and send the SuiteBegin event
348
+ const request = new protobuf_1.events.SuiteBeginEventRequest({
349
+ suite: new protobuf_1.entities.TestSuiteRun({
350
+ id: id,
351
+ name: suite.title || 'root',
352
+ start_time: (0, utils_1.createTimestamp)(this.runStartTime),
353
+ metadata: metadata,
354
+ }),
355
+ });
356
+ // Fire-and-forget to avoid slowing tests
357
+ this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportSuiteBegin", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report suite begin", e));
358
+ }
359
+ // Helper: Report suite end event
360
+ reportSuiteEnd(suite, result) {
361
+ const suiteId = `${this.runId}-suite-${suite.title || 'root'}`;
362
+ const endTime = new Date();
363
+ const duration = endTime.getTime() - this.runStartTime.getTime();
364
+ console.log(`Stanterprise Reporter: Suite ended - ${suite.title || 'root'}`);
365
+ console.log(` Duration: ${duration}ms`);
366
+ // Map result status to protobuf TestStatus
367
+ const suiteStatus = this.mapSuiteStatus(result.status);
368
+ // Build metadata from suite location
369
+ const metadata = new Map();
370
+ metadata.set("suite_type", suite.type);
371
+ metadata.set("final_result", result.status);
372
+ if (suite.location) {
373
+ metadata.set("location_file", suite.location.file);
374
+ metadata.set("location_line", suite.location.line.toString());
375
+ metadata.set("location_column", suite.location.column.toString());
376
+ }
377
+ // Build and send the SuiteEnd event
378
+ const request = new protobuf_1.events.SuiteEndEventRequest({
379
+ suite: new protobuf_1.entities.TestSuiteRun({
380
+ id: suiteId,
381
+ name: suite.title || 'root',
382
+ start_time: (0, utils_1.createTimestamp)(this.runStartTime),
383
+ end_time: (0, utils_1.createTimestamp)(endTime),
384
+ duration: (0, utils_1.createDuration)(duration),
385
+ status: suiteStatus,
386
+ metadata: metadata,
387
+ }),
388
+ });
389
+ // Fire-and-forget to avoid slowing tests
390
+ this.reportUnary("/testsystem.v1.observer.TestEventCollector/ReportSuiteEnd", request, this.grpcTimeout).catch((e) => this.logGrpcErrorOnce("Failed to report suite end", e));
391
+ }
392
+ // Helper: Map FullResult status to protobuf TestStatus
393
+ mapSuiteStatus(status) {
394
+ switch (status) {
395
+ case "passed":
396
+ return protobuf_1.common.TestStatus.PASSED;
397
+ case "failed":
398
+ return protobuf_1.common.TestStatus.FAILED;
399
+ case "timedout":
400
+ return protobuf_1.common.TestStatus.FAILED;
401
+ case "interrupted":
402
+ return protobuf_1.common.TestStatus.BROKEN;
403
+ default:
404
+ return protobuf_1.common.TestStatus.UNKNOWN;
405
+ }
406
+ }
407
+ // Helper: log first gRPC error once and disable further attempts
408
+ logGrpcErrorOnce(prefix, err) {
409
+ if (this.grpcFirstErrorLogged)
410
+ return;
411
+ this.grpcFirstErrorLogged = true;
412
+ const details = err instanceof Error ? `${err.message}` : String(err);
413
+ console.warn(`${prefix}. gRPC disabled for the remainder of the run. Address=${this.grpcAddress}. Details: ${details}`);
414
+ this.grpcEnabled = false;
415
+ }
416
+ // Helper: generic unary call using raw method path
417
+ async reportUnary(path, message, deadlineMs = 1000) {
418
+ if (!this.grpcEnabled || !this.grpcClient) {
419
+ return Buffer.alloc(0);
420
+ }
421
+ const reqSerialize = (arg) => {
422
+ const m = arg;
423
+ const bytes = m?.serializeBinary
424
+ ? m.serializeBinary()
425
+ : m?.serialize
426
+ ? m.serialize()
427
+ : new Uint8Array(0);
428
+ return Buffer.from(bytes);
429
+ };
430
+ const resDeserialize = (bytes) => bytes;
431
+ const metadata = new grpc.Metadata();
432
+ const callOptions = {
433
+ deadline: new Date(Date.now() + deadlineMs),
434
+ };
435
+ return new Promise((resolve, reject) => {
436
+ try {
437
+ this.grpcClient.makeUnaryRequest(path, reqSerialize, resDeserialize, message, metadata, callOptions, (err, response) => {
438
+ if (err)
439
+ return reject(err);
440
+ resolve(response);
441
+ });
442
+ }
443
+ catch (e) {
444
+ reject(e);
445
+ }
446
+ });
447
+ }
448
+ }
449
+ exports.default = StanterpriseReporter;
@@ -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,5 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for Stanterprise Playwright Reporter
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Utility functions for processing test attachments
3
+ */
4
+ import type { TestResult } from "@playwright/test/reporter";
5
+ import { common } from "@stanterprise/protobuf";
6
+ /**
7
+ * Process Playwright test attachments into protobuf Attachment objects
8
+ */
9
+ export declare function processAttachments(result: TestResult): InstanceType<typeof common.Attachment>[];
10
+ /**
11
+ * Extract error information from test results
12
+ */
13
+ export declare function extractErrorInfo(result: TestResult): {
14
+ errorMessage: string;
15
+ stackTrace: string;
16
+ errors: string[];
17
+ };
@@ -0,0 +1,43 @@
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
+ /**
7
+ * Process Playwright test attachments into protobuf Attachment objects
8
+ */
9
+ function processAttachments(result) {
10
+ const attachments = [];
11
+ if (!result.attachments || result.attachments.length === 0) {
12
+ return attachments;
13
+ }
14
+ for (const attachment of result.attachments) {
15
+ const att = new protobuf_1.common.Attachment({
16
+ name: attachment.name,
17
+ mime_type: attachment.contentType,
18
+ });
19
+ // Use path as URI if available, otherwise use the body content
20
+ if (attachment.path) {
21
+ att.uri = attachment.path;
22
+ }
23
+ else if (attachment.body) {
24
+ att.content = attachment.body;
25
+ }
26
+ attachments.push(att);
27
+ }
28
+ return attachments;
29
+ }
30
+ /**
31
+ * Extract error information from test results
32
+ */
33
+ function extractErrorInfo(result) {
34
+ let errorMessage = "";
35
+ let stackTrace = "";
36
+ const errors = [];
37
+ if (result.errors && result.errors.length > 0) {
38
+ errorMessage = result.errors.map((e) => e.message || "").join("\n");
39
+ stackTrace = result.errors.map((e) => e.stack || "").join("\n");
40
+ errors.push(...result.errors.map((e) => e.message || ""));
41
+ }
42
+ return { errorMessage, stackTrace, errors };
43
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utility exports - centralized access to all utility functions
3
+ */
4
+ export * from "./statusMapper";
5
+ export * from "./attachmentProcessor";
6
+ export * from "./timeHelpers";
@@ -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,28 @@
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
+ /**
7
+ * Map Playwright test status to protobuf TestStatus
8
+ */
9
+ function mapTestStatus(status) {
10
+ switch (status) {
11
+ case "passed":
12
+ return protobuf_1.common.TestStatus.PASSED;
13
+ case "failed":
14
+ return protobuf_1.common.TestStatus.FAILED;
15
+ case "skipped":
16
+ return protobuf_1.common.TestStatus.SKIPPED;
17
+ case "timedOut":
18
+ return protobuf_1.common.TestStatus.FAILED; // Treat timeout as failed
19
+ default:
20
+ return protobuf_1.common.TestStatus.UNKNOWN;
21
+ }
22
+ }
23
+ /**
24
+ * Map step error to test status
25
+ */
26
+ function mapStepStatus(hasError) {
27
+ return hasError ? protobuf_1.common.TestStatus.FAILED : protobuf_1.common.TestStatus.PASSED;
28
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Utility functions for creating timestamps and durations
3
+ */
4
+ import { google } from "@stanterprise/protobuf";
5
+ import type { google as googleTypes } from "@stanterprise/protobuf/dist/lib/google/protobuf/duration";
6
+ /**
7
+ * Create a protobuf Timestamp from a JavaScript Date
8
+ */
9
+ export declare function createTimestamp(date: Date): InstanceType<typeof google.protobuf.Timestamp>;
10
+ /**
11
+ * Create a protobuf Timestamp from milliseconds since epoch
12
+ */
13
+ export declare function createTimestampFromMs(ms: number): InstanceType<typeof google.protobuf.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
+ * The return type is cast to Duration for proper type checking with the schema.
22
+ *
23
+ * @param durationMs Duration in milliseconds
24
+ * @returns A Duration object (implemented as Timestamp due to export limitations)
25
+ */
26
+ export declare function createDuration(durationMs: number): googleTypes.protobuf.Duration;
@@ -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
+ /**
11
+ * Create a protobuf Timestamp from a JavaScript Date
12
+ */
13
+ function createTimestamp(date) {
14
+ return new protobuf_1.google.protobuf.Timestamp({
15
+ seconds: Math.floor(date.getTime() / 1000),
16
+ nanos: (date.getTime() % 1000) * 1000000,
17
+ });
18
+ }
19
+ /**
20
+ * Create a protobuf Timestamp from milliseconds since epoch
21
+ */
22
+ function createTimestampFromMs(ms) {
23
+ return new protobuf_1.google.protobuf.Timestamp({
24
+ seconds: Math.floor(ms / 1000),
25
+ nanos: (ms % 1000) * 1000000,
26
+ });
27
+ }
28
+ /**
29
+ * Create a duration object from milliseconds.
30
+ *
31
+ * NOTE: google.protobuf.Duration is defined in the @stanterprise/protobuf schema
32
+ * but not exported in the package's main barrel export. Since Duration and Timestamp
33
+ * have identical structure (seconds: number, nanos: number) and the protobuf serialization
34
+ * is wire-compatible, we use Timestamp as a Duration substitute. This works correctly at runtime.
35
+ * The return type is cast to Duration for proper type checking with the schema.
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 protobuf_1.google.protobuf.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": "1.0.0",
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": "^30.0.0",
48
+ "@types/node": "^22.15.29",
49
+ "jest": "^30.2.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.9"
58
+ }
59
+ }