@testpulse.run/reporter 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/fixtures/createTestPulseTest.js +7 -9
- package/dist/fixtures/options.d.ts +1 -1
- package/dist/fixtures/options.js +16 -28
- package/dist/fixtures/types.d.ts +0 -2
- package/dist/live/EncoderProcess.d.ts +2 -2
- package/dist/live/EncoderProcess.js +4 -4
- package/dist/live/LiveCaptureService.js +1 -1
- package/dist/live/types.d.ts +1 -0
- package/dist/reporter/GitRuntimeMetadataResolver.js +0 -2
- package/dist/reporter/PlaywrightReporter.d.ts +1 -0
- package/dist/reporter/PlaywrightReporter.js +33 -18
- package/dist/reporter/types.d.ts +1 -3
- package/dist/shared/endpoint.d.ts +1 -1
- package/dist/shared/endpoint.js +3 -17
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @testpulse.run/reporter
|
|
2
2
|
|
|
3
|
-
Playwright reporter for streaming TestPulse runs and
|
|
3
|
+
Playwright reporter for streaming TestPulse runs and live browser capture.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -35,4 +35,4 @@ test("streams automatically when page is used", async ({ page }) => {
|
|
|
35
35
|
});
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Live streaming is
|
|
38
|
+
Live streaming is always on when the reporter is configured and the fixture automatically starts and stops live capture for tests that use the `page` fixture. The package bundles `ffmpeg` automatically. To override the encoder path, pass `ffmpegPath` in the fixture options.
|
|
@@ -5,16 +5,14 @@ export function createAutoLiveCapturePageFixture(options = {}, startCapture = st
|
|
|
5
5
|
return async ({ page }, use, testInfo) => {
|
|
6
6
|
const resolved = resolveTestPulseFixturesOptions(options);
|
|
7
7
|
let capture = null;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
console.warn(`[TestPulse] Auto live capture skipped for test "${testInfo.title}": ${error instanceof Error ? error.message : String(error)}`);
|
|
8
|
+
try {
|
|
9
|
+
capture = await startCapture(page, resolved.captureOptions);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (!resolved.swallowErrors) {
|
|
13
|
+
throw error;
|
|
17
14
|
}
|
|
15
|
+
console.warn(`[TestPulse] Auto live capture skipped for test "${testInfo.title}": ${error instanceof Error ? error.message : String(error)}`);
|
|
18
16
|
}
|
|
19
17
|
try {
|
|
20
18
|
await use(page);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ResolvedTestPulseFixturesOptions, TestPulseFixturesOptions } from "./types.js";
|
|
2
|
-
export declare function resolveTestPulseFixturesOptions(options?: TestPulseFixturesOptions
|
|
2
|
+
export declare function resolveTestPulseFixturesOptions(options?: TestPulseFixturesOptions): ResolvedTestPulseFixturesOptions;
|
package/dist/fixtures/options.js
CHANGED
|
@@ -1,45 +1,33 @@
|
|
|
1
1
|
import { resolveTestPulseEndpoint } from "../shared/endpoint.js";
|
|
2
|
-
export function resolveTestPulseFixturesOptions(options = {}
|
|
3
|
-
const enabled = options.enabled ?? parseOptionalBoolean(environment.TESTPULSE_ENABLE_LIVE_STREAMING) ?? true;
|
|
2
|
+
export function resolveTestPulseFixturesOptions(options = {}) {
|
|
4
3
|
const swallowErrors = options.swallowErrors ?? true;
|
|
5
4
|
return {
|
|
6
|
-
enabled,
|
|
7
5
|
swallowErrors,
|
|
8
6
|
captureOptions: {
|
|
9
|
-
endpoint: resolveTestPulseEndpoint(options.endpoint
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
endpoint: resolveTestPulseEndpoint(options.endpoint),
|
|
8
|
+
projectId: trim(options.projectId),
|
|
9
|
+
apiKey: trim(options.apiKey),
|
|
10
|
+
runId: trim(options.runId),
|
|
11
|
+
streamPublisherToken: trim(options.streamPublisherToken),
|
|
12
|
+
sessionKey: trim(options.sessionKey),
|
|
13
|
+
stateDirectory: trim(options.stateDirectory),
|
|
14
|
+
intervalMs: normalizeOptionalNumber(options.intervalMs),
|
|
15
|
+
profile: parseProfile(options.profile),
|
|
16
|
+
ffmpegPath: trim(options.ffmpegPath)
|
|
16
17
|
}
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
function parseOptionalBoolean(value) {
|
|
20
|
-
const normalized = value?.trim().toLowerCase();
|
|
21
|
-
if (!normalized) {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
if (normalized === "1" || normalized === "true") {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
if (normalized === "0" || normalized === "false") {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
20
|
function trim(value) {
|
|
33
21
|
const trimmed = value?.trim();
|
|
34
22
|
return trimmed ? trimmed : undefined;
|
|
35
23
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
if (!trimmed) {
|
|
24
|
+
function normalizeOptionalNumber(value) {
|
|
25
|
+
if (value === undefined) {
|
|
39
26
|
return undefined;
|
|
40
27
|
}
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
return Number.isFinite(value) && value > 0
|
|
29
|
+
? value
|
|
30
|
+
: undefined;
|
|
43
31
|
}
|
|
44
32
|
function parseProfile(value) {
|
|
45
33
|
return value === "focus" || value === "grid"
|
package/dist/fixtures/types.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { LiveCaptureOptions } from "../live/types.js";
|
|
2
2
|
export interface TestPulseFixturesOptions extends LiveCaptureOptions {
|
|
3
|
-
enabled?: boolean;
|
|
4
3
|
swallowErrors?: boolean;
|
|
5
4
|
}
|
|
6
5
|
export interface ResolvedTestPulseFixturesOptions {
|
|
7
|
-
enabled: boolean;
|
|
8
6
|
swallowErrors: boolean;
|
|
9
7
|
captureOptions: LiveCaptureOptions;
|
|
10
8
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import type { StreamDescriptor } from "./types.js";
|
|
3
|
-
export declare function startEncoderProcess(descriptor: StreamDescriptor): ChildProcessWithoutNullStreams;
|
|
4
|
-
export declare function resolveFfmpegPath(): string;
|
|
3
|
+
export declare function startEncoderProcess(descriptor: StreamDescriptor, ffmpegPath?: string): ChildProcessWithoutNullStreams;
|
|
4
|
+
export declare function resolveFfmpegPath(ffmpegPath?: string): string;
|
|
5
5
|
export declare function waitForEncoder(encoder: ChildProcessWithoutNullStreams): Promise<void>;
|
|
@@ -2,9 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { once } from "node:events";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
|
-
export function startEncoderProcess(descriptor) {
|
|
5
|
+
export function startEncoderProcess(descriptor, ffmpegPath) {
|
|
6
6
|
const gopFrames = Math.max(1, Math.round(descriptor.framesPerSecond * descriptor.gopDurationMs / 1000));
|
|
7
|
-
const ffmpeg = spawn(resolveFfmpegPath(), [
|
|
7
|
+
const ffmpeg = spawn(resolveFfmpegPath(ffmpegPath), [
|
|
8
8
|
"-loglevel", "error",
|
|
9
9
|
"-f", "image2pipe",
|
|
10
10
|
"-r", String(descriptor.framesPerSecond),
|
|
@@ -36,8 +36,8 @@ export function startEncoderProcess(descriptor) {
|
|
|
36
36
|
});
|
|
37
37
|
return ffmpeg;
|
|
38
38
|
}
|
|
39
|
-
export function resolveFfmpegPath() {
|
|
40
|
-
const configuredPath =
|
|
39
|
+
export function resolveFfmpegPath(ffmpegPath) {
|
|
40
|
+
const configuredPath = ffmpegPath?.trim();
|
|
41
41
|
if (configuredPath) {
|
|
42
42
|
return configuredPath;
|
|
43
43
|
}
|
|
@@ -14,7 +14,7 @@ export async function startLiveCapture(page, options = {}) {
|
|
|
14
14
|
const streamId = randomUUID();
|
|
15
15
|
const captureStartedAt = Date.now();
|
|
16
16
|
const logTimingOnce = createTimingLogger(captureStartedAt);
|
|
17
|
-
const encoder = startEncoderProcess(descriptor);
|
|
17
|
+
const encoder = startEncoderProcess(descriptor, options.ffmpegPath);
|
|
18
18
|
const uploader = new StreamUploader(session, streamId, descriptor, captureStartedAt);
|
|
19
19
|
const uploadQueue = new SegmentQueue((error) => {
|
|
20
20
|
console.warn(`[TestPulse] Live streaming disabled for run ${session.runId}: ${error.message}`);
|
package/dist/live/types.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
4
|
const branchEnvironmentVariables = [
|
|
5
|
-
"TESTPULSE_BRANCH",
|
|
6
5
|
"GITHUB_HEAD_REF",
|
|
7
6
|
"GITHUB_REF_NAME",
|
|
8
7
|
"CI_COMMIT_REF_NAME",
|
|
@@ -10,7 +9,6 @@ const branchEnvironmentVariables = [
|
|
|
10
9
|
"BRANCH_NAME"
|
|
11
10
|
];
|
|
12
11
|
const commitEnvironmentVariables = [
|
|
13
|
-
"TESTPULSE_COMMIT_SHA",
|
|
14
12
|
"GITHUB_SHA",
|
|
15
13
|
"CI_COMMIT_SHA",
|
|
16
14
|
"BUILD_SOURCEVERSION",
|
|
@@ -3,6 +3,7 @@ import type { TestPulseReporterOptions } from "./types.js";
|
|
|
3
3
|
export default class TestPulseReporter implements Reporter {
|
|
4
4
|
private readonly options;
|
|
5
5
|
private runId;
|
|
6
|
+
private isListOnly;
|
|
6
7
|
private metadataPromise;
|
|
7
8
|
private readonly eventQueue;
|
|
8
9
|
private readonly runClient;
|
|
@@ -7,6 +7,7 @@ import { RunClient } from "./RunClient.js";
|
|
|
7
7
|
export default class TestPulseReporter {
|
|
8
8
|
options;
|
|
9
9
|
runId = null;
|
|
10
|
+
isListOnly = false;
|
|
10
11
|
metadataPromise = null;
|
|
11
12
|
eventQueue = new EventQueue();
|
|
12
13
|
runClient;
|
|
@@ -16,16 +17,22 @@ export default class TestPulseReporter {
|
|
|
16
17
|
this.options = options;
|
|
17
18
|
this.resolvedOptions = {
|
|
18
19
|
...options,
|
|
19
|
-
endpoint: resolveTestPulseEndpoint(options.endpoint)
|
|
20
|
-
liveStreamingEnabled: options.liveStreamingEnabled ?? true
|
|
20
|
+
endpoint: resolveTestPulseEndpoint(options.endpoint)
|
|
21
21
|
};
|
|
22
22
|
this.runClient = new RunClient(this.resolvedOptions);
|
|
23
23
|
this.artifactUploader = new ArtifactUploader(this.eventQueue, () => this.ensureRunStarted(), this.runClient);
|
|
24
24
|
}
|
|
25
25
|
async onBegin(_config, _suite) {
|
|
26
|
+
this.isListOnly = Boolean(_config.cliListOnly);
|
|
27
|
+
if (this.isListOnly) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
26
30
|
await this.ensureRunStarted();
|
|
27
31
|
}
|
|
28
32
|
onTestBegin(test) {
|
|
33
|
+
if (this.isListOnly) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
29
36
|
this.enqueueEvent("test.started", {
|
|
30
37
|
testId: test.id,
|
|
31
38
|
testName: test.title,
|
|
@@ -33,6 +40,9 @@ export default class TestPulseReporter {
|
|
|
33
40
|
});
|
|
34
41
|
}
|
|
35
42
|
onTestEnd(test, result) {
|
|
43
|
+
if (this.isListOnly) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
36
46
|
const eventType = result.status === "passed"
|
|
37
47
|
? "test.passed"
|
|
38
48
|
: result.status === "skipped"
|
|
@@ -55,6 +65,9 @@ export default class TestPulseReporter {
|
|
|
55
65
|
this.artifactUploader.enqueueArtifacts(test, result, eventSequence);
|
|
56
66
|
}
|
|
57
67
|
onStdOut(chunk, test) {
|
|
68
|
+
if (this.isListOnly) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
58
71
|
this.enqueueEvent("log", {
|
|
59
72
|
level: "info",
|
|
60
73
|
message: chunk.toString(),
|
|
@@ -62,6 +75,9 @@ export default class TestPulseReporter {
|
|
|
62
75
|
});
|
|
63
76
|
}
|
|
64
77
|
onStdErr(chunk, test) {
|
|
78
|
+
if (this.isListOnly) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
65
81
|
this.enqueueEvent("log", {
|
|
66
82
|
level: "error",
|
|
67
83
|
message: chunk.toString(),
|
|
@@ -69,15 +85,16 @@ export default class TestPulseReporter {
|
|
|
69
85
|
});
|
|
70
86
|
}
|
|
71
87
|
async onEnd(result) {
|
|
88
|
+
if (this.isListOnly || !this.runId) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
72
91
|
const runId = await this.ensureRunStarted();
|
|
73
92
|
await this.eventQueue.flush();
|
|
74
93
|
try {
|
|
75
94
|
await this.runClient.completeRun(runId, result.status === "failed" ? "Failed" : "Completed");
|
|
76
95
|
}
|
|
77
96
|
finally {
|
|
78
|
-
|
|
79
|
-
await deleteLiveCaptureState(this.options.liveStreamSessionKey, this.options.liveCaptureStateDirectory);
|
|
80
|
-
}
|
|
97
|
+
await deleteLiveCaptureState(this.options.liveStreamSessionKey, this.options.liveCaptureStateDirectory);
|
|
81
98
|
}
|
|
82
99
|
}
|
|
83
100
|
printsToStdio() {
|
|
@@ -108,20 +125,18 @@ export default class TestPulseReporter {
|
|
|
108
125
|
const metadata = await this.metadataPromise;
|
|
109
126
|
const payload = await this.runClient.startRun(metadata);
|
|
110
127
|
this.runId = payload.runId;
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
throw new Error("TestPulse live streaming token was not returned by the backend.");
|
|
114
|
-
}
|
|
115
|
-
await writeLiveCaptureState({
|
|
116
|
-
endpoint: this.resolvedOptions.endpoint,
|
|
117
|
-
projectId: this.options.projectId,
|
|
118
|
-
apiKey: this.options.apiKey,
|
|
119
|
-
runId: this.runId,
|
|
120
|
-
streamPublisherToken: payload.streamPublisherToken,
|
|
121
|
-
createdAt: new Date().toISOString(),
|
|
122
|
-
intervalMs: this.options.liveCaptureIntervalMs
|
|
123
|
-
}, this.options.liveStreamSessionKey, this.options.liveCaptureStateDirectory);
|
|
128
|
+
if (!payload.streamPublisherToken) {
|
|
129
|
+
throw new Error("TestPulse live streaming token was not returned by the backend.");
|
|
124
130
|
}
|
|
131
|
+
await writeLiveCaptureState({
|
|
132
|
+
endpoint: this.resolvedOptions.endpoint,
|
|
133
|
+
projectId: this.options.projectId,
|
|
134
|
+
apiKey: this.options.apiKey,
|
|
135
|
+
runId: this.runId,
|
|
136
|
+
streamPublisherToken: payload.streamPublisherToken,
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
intervalMs: this.options.liveCaptureIntervalMs
|
|
139
|
+
}, this.options.liveStreamSessionKey, this.options.liveCaptureStateDirectory);
|
|
125
140
|
return this.runId;
|
|
126
141
|
}
|
|
127
142
|
}
|
package/dist/reporter/types.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export interface TestPulseReporterOptions {
|
|
|
5
5
|
runName?: string;
|
|
6
6
|
branch?: string;
|
|
7
7
|
commitSha?: string;
|
|
8
|
-
liveStreamingEnabled?: boolean;
|
|
9
8
|
liveCaptureIntervalMs?: number;
|
|
10
9
|
liveStreamSessionKey?: string;
|
|
11
10
|
liveCaptureStateDirectory?: string;
|
|
@@ -21,7 +20,6 @@ export type StartRunResponse = {
|
|
|
21
20
|
runId: string;
|
|
22
21
|
streamPublisherToken?: string;
|
|
23
22
|
};
|
|
24
|
-
export interface ResolvedTestPulseReporterOptions extends Omit<TestPulseReporterOptions, "endpoint"
|
|
23
|
+
export interface ResolvedTestPulseReporterOptions extends Omit<TestPulseReporterOptions, "endpoint"> {
|
|
25
24
|
endpoint: string;
|
|
26
|
-
liveStreamingEnabled: boolean;
|
|
27
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function resolveTestPulseEndpoint(endpoint: string | undefined
|
|
1
|
+
export declare function resolveTestPulseEndpoint(endpoint: string | undefined): string;
|
package/dist/shared/endpoint.js
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
local: "http://localhost:5123"
|
|
5
|
-
};
|
|
6
|
-
export function resolveTestPulseEndpoint(endpoint, environment = process.env) {
|
|
7
|
-
const explicitEndpoint = trim(endpoint) ?? trim(environment.TESTPULSE_ENDPOINT);
|
|
8
|
-
if (explicitEndpoint) {
|
|
9
|
-
return trimTrailingSlash(explicitEndpoint);
|
|
10
|
-
}
|
|
11
|
-
return defaultEndpoints[parseEnvironment(environment.TESTPULSE_ENV)];
|
|
12
|
-
}
|
|
13
|
-
function parseEnvironment(value) {
|
|
14
|
-
const normalized = value?.trim().toLowerCase();
|
|
15
|
-
return normalized === "local" || normalized === "staging"
|
|
16
|
-
? normalized
|
|
17
|
-
: "production";
|
|
1
|
+
const defaultEndpoint = "https://testpulse.mevodo.com";
|
|
2
|
+
export function resolveTestPulseEndpoint(endpoint) {
|
|
3
|
+
return trimTrailingSlash(trim(endpoint) ?? defaultEndpoint);
|
|
18
4
|
}
|
|
19
5
|
function trim(value) {
|
|
20
6
|
const trimmed = value?.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testpulse.run/reporter",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Playwright reporter for streaming TestPulse runs and optional live browser capture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"@playwright/test": ">=1.40.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"ffmpeg-static": "^5.3.0"
|
|
56
|
+
"ffmpeg-static": "^5.3.0",
|
|
57
|
+
"mv-testpulse-demo": "file:../../demo"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@playwright/test": "^1.54.2",
|