@lokalise/playwright-reporters 1.8.0 → 1.9.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/README.md +1 -0
- package/dist/analytics/elastic.js +7 -0
- package/dist/analytics/report.types.d.ts +15 -0
- package/dist/analytics/reporter.js +14 -0
- package/dist/analytics/transform.d.ts +1 -0
- package/dist/datadog/datadog.d.ts +25 -0
- package/dist/datadog/datadog.js +86 -0
- package/dist/datadog/index.d.ts +2 -0
- package/dist/datadog/index.js +9 -0
- package/dist/datadog/report.types.d.ts +175 -0
- package/dist/datadog/report.types.js +9 -0
- package/dist/datadog/reporter.d.ts +35 -0
- package/dist/datadog/reporter.js +296 -0
- package/dist/datadog/tests/e2e/browser.test.d.ts +1 -0
- package/dist/datadog/tests/e2e/browser.test.js +49 -0
- package/dist/datadog/tests/e2e/sample.test.d.ts +1 -0
- package/dist/datadog/tests/e2e/sample.test.js +69 -0
- package/dist/datadog/transform.d.ts +26 -0
- package/dist/datadog/transform.js +185 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +36 -23
- package/dist/permissions/elastic.js +2 -0
- package/dist/permissions/reporter.js +4 -0
- package/dist/retry/reporter.js +7 -0
- package/dist/stepDuration/reporter.js +3 -0
- package/dist/timeline/reporter.js +2 -0
- package/dist/translation/elastic.js +2 -0
- package/dist/translation/reporter.js +6 -0
- package/dist/translation/tests/transform.test.js +60 -11
- package/package.json +26 -19
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineDatadogReporterConfig = void 0;
|
|
4
|
+
/* eslint-disable max-statements */
|
|
5
|
+
const faker_1 = require("@faker-js/faker");
|
|
6
|
+
const datadog_1 = require("./datadog");
|
|
7
|
+
const transform_1 = require("./transform");
|
|
8
|
+
/**
|
|
9
|
+
* Extracts team name from test annotations
|
|
10
|
+
*/
|
|
11
|
+
const getTeamName = (test) => {
|
|
12
|
+
const ownerAnnotation = test.annotations?.find((annotation) => annotation.type === 'owner');
|
|
13
|
+
return ownerAnnotation?.description?.split(',')[0];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Playwright reporter that sends test execution metrics to Datadog
|
|
17
|
+
*/
|
|
18
|
+
// eslint-disable-next-line import/no-default-export
|
|
19
|
+
class DatadogReporter {
|
|
20
|
+
options;
|
|
21
|
+
datadog;
|
|
22
|
+
testRunId;
|
|
23
|
+
// Counters
|
|
24
|
+
runStartTime;
|
|
25
|
+
runEndTime;
|
|
26
|
+
totalTests;
|
|
27
|
+
passedTests;
|
|
28
|
+
failedTests;
|
|
29
|
+
flakyTests;
|
|
30
|
+
skippedTests;
|
|
31
|
+
timedOutTests;
|
|
32
|
+
browserCreationMs;
|
|
33
|
+
retryTimeMs;
|
|
34
|
+
navigationMs;
|
|
35
|
+
workerCount;
|
|
36
|
+
testDurations;
|
|
37
|
+
// Per-test tracking
|
|
38
|
+
testMetrics;
|
|
39
|
+
projectNames;
|
|
40
|
+
currentTestNavigationMs;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
this.testRunId = faker_1.faker.string.uuid();
|
|
44
|
+
this.datadog = new datadog_1.Datadog({
|
|
45
|
+
apiKey: options.apiKey,
|
|
46
|
+
site: options.site,
|
|
47
|
+
debug: options.debug,
|
|
48
|
+
});
|
|
49
|
+
// Initialize counters
|
|
50
|
+
this.runStartTime = Date.now();
|
|
51
|
+
this.runEndTime = Date.now();
|
|
52
|
+
this.totalTests = 0;
|
|
53
|
+
this.passedTests = 0;
|
|
54
|
+
this.failedTests = 0;
|
|
55
|
+
this.flakyTests = 0;
|
|
56
|
+
this.skippedTests = 0;
|
|
57
|
+
this.timedOutTests = 0;
|
|
58
|
+
this.browserCreationMs = 0;
|
|
59
|
+
this.retryTimeMs = 0;
|
|
60
|
+
this.navigationMs = 0;
|
|
61
|
+
this.workerCount = 0;
|
|
62
|
+
this.testDurations = [];
|
|
63
|
+
this.testMetrics = [];
|
|
64
|
+
this.projectNames = [];
|
|
65
|
+
this.currentTestNavigationMs = new Map();
|
|
66
|
+
}
|
|
67
|
+
async onBegin(config, suite) {
|
|
68
|
+
try {
|
|
69
|
+
this.runStartTime = Date.now();
|
|
70
|
+
this.totalTests = suite.allTests().length;
|
|
71
|
+
this.projectNames = suite.suites.map((testSuite) => testSuite.title);
|
|
72
|
+
this.workerCount = config.workers;
|
|
73
|
+
if (!this.options.dryRun) {
|
|
74
|
+
await this.datadog.setNativeRequestContext();
|
|
75
|
+
}
|
|
76
|
+
if (this.options.debug) {
|
|
77
|
+
// eslint-disable-next-line no-console
|
|
78
|
+
console.log(`[Datadog Reporter] Starting test run ${this.testRunId} with ${this.totalTests} tests (${this.workerCount} workers)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.error("[Datadog Reporter] Could not perform 'onBegin' step", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
onStepEnd(test, result, step) {
|
|
87
|
+
try {
|
|
88
|
+
const testKey = `${test.id}-${result.retry}`;
|
|
89
|
+
const stepTitle = step.title.toLowerCase();
|
|
90
|
+
// Track browser/context/page creation time
|
|
91
|
+
// Captures: "Launch browser", "Create context", "Create page"
|
|
92
|
+
if (stepTitle.includes('launch browser') ||
|
|
93
|
+
stepTitle.includes('create context') ||
|
|
94
|
+
stepTitle.includes('create page') ||
|
|
95
|
+
stepTitle.includes('browsercontext.newpage') ||
|
|
96
|
+
stepTitle.includes('browser.newcontext')) {
|
|
97
|
+
this.browserCreationMs += step.duration;
|
|
98
|
+
}
|
|
99
|
+
// Track page navigation time
|
|
100
|
+
// Captures: "Navigate to ..." (Playwright's actual step title for goto)
|
|
101
|
+
if (stepTitle.includes('navigate to') ||
|
|
102
|
+
stepTitle.includes('page.goto') ||
|
|
103
|
+
stepTitle.includes('page.reload') ||
|
|
104
|
+
stepTitle.includes('frame.goto')) {
|
|
105
|
+
this.navigationMs += step.duration;
|
|
106
|
+
const currentNav = this.currentTestNavigationMs.get(testKey) || 0;
|
|
107
|
+
this.currentTestNavigationMs.set(testKey, currentNav + step.duration);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// eslint-disable-next-line no-console
|
|
112
|
+
console.error("[Datadog Reporter] Could not perform 'onStepEnd' step", error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
onTestEnd(test, result) {
|
|
116
|
+
try {
|
|
117
|
+
const { status, retry: currentRetryCount, duration } = result;
|
|
118
|
+
const { retries: maxRetriesCount } = test;
|
|
119
|
+
const projectName = test.parent?.project()?.name;
|
|
120
|
+
const testKey = `${test.id}-${currentRetryCount}`;
|
|
121
|
+
const testNavigationMs = this.currentTestNavigationMs.get(testKey) || 0;
|
|
122
|
+
// Track test duration for average calculation (only on final attempt)
|
|
123
|
+
if (currentRetryCount === 0 || currentRetryCount === maxRetriesCount) {
|
|
124
|
+
this.testDurations.push(duration);
|
|
125
|
+
}
|
|
126
|
+
// Track test outcome
|
|
127
|
+
if (status === 'skipped') {
|
|
128
|
+
this.skippedTests++;
|
|
129
|
+
}
|
|
130
|
+
else if (status === 'timedOut') {
|
|
131
|
+
// Track timeout separately
|
|
132
|
+
if (currentRetryCount === maxRetriesCount) {
|
|
133
|
+
this.timedOutTests++;
|
|
134
|
+
this.failedTests++;
|
|
135
|
+
this.testMetrics.push({
|
|
136
|
+
name: test.title,
|
|
137
|
+
testId: test.id,
|
|
138
|
+
durationMs: duration,
|
|
139
|
+
retryCount: currentRetryCount,
|
|
140
|
+
isFlaky: false,
|
|
141
|
+
isTimedOut: true,
|
|
142
|
+
team: getTeamName(test),
|
|
143
|
+
project: projectName,
|
|
144
|
+
navigationMs: testNavigationMs,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (currentRetryCount > 0) {
|
|
148
|
+
this.retryTimeMs += duration;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (['failed', 'interrupted'].includes(status)) {
|
|
152
|
+
// Only count as failed if all retries exhausted
|
|
153
|
+
if (currentRetryCount === maxRetriesCount) {
|
|
154
|
+
this.failedTests++;
|
|
155
|
+
// Track failed test metrics
|
|
156
|
+
this.testMetrics.push({
|
|
157
|
+
name: test.title,
|
|
158
|
+
testId: test.id,
|
|
159
|
+
durationMs: duration,
|
|
160
|
+
retryCount: currentRetryCount,
|
|
161
|
+
isFlaky: false,
|
|
162
|
+
isTimedOut: false,
|
|
163
|
+
team: getTeamName(test),
|
|
164
|
+
project: projectName,
|
|
165
|
+
navigationMs: testNavigationMs,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Track retry time
|
|
169
|
+
if (currentRetryCount > 0) {
|
|
170
|
+
this.retryTimeMs += duration;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (status === 'passed') {
|
|
174
|
+
if (currentRetryCount > 0) {
|
|
175
|
+
// Flaky test - passed after retry
|
|
176
|
+
this.flakyTests++;
|
|
177
|
+
this.passedTests++;
|
|
178
|
+
this.retryTimeMs += duration;
|
|
179
|
+
// Track flaky test metrics
|
|
180
|
+
this.testMetrics.push({
|
|
181
|
+
name: test.title,
|
|
182
|
+
testId: test.id,
|
|
183
|
+
durationMs: duration,
|
|
184
|
+
retryCount: currentRetryCount,
|
|
185
|
+
isFlaky: true,
|
|
186
|
+
isTimedOut: false,
|
|
187
|
+
team: getTeamName(test),
|
|
188
|
+
project: projectName,
|
|
189
|
+
navigationMs: testNavigationMs,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Clean pass
|
|
194
|
+
this.passedTests++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// eslint-disable-next-line no-console
|
|
200
|
+
console.error("[Datadog Reporter] Could not perform 'onTestEnd' step", error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async onEnd(result) {
|
|
204
|
+
try {
|
|
205
|
+
this.runEndTime = Date.now();
|
|
206
|
+
// Calculate average test duration
|
|
207
|
+
const avgTestDurationMs = this.testDurations.length > 0
|
|
208
|
+
? Math.round(this.testDurations.reduce((a, b) => a + b, 0) / this.testDurations.length)
|
|
209
|
+
: 0;
|
|
210
|
+
// Build run metrics
|
|
211
|
+
const runMetrics = {
|
|
212
|
+
testRunId: this.testRunId,
|
|
213
|
+
timestamp: Math.floor(this.runStartTime / 1000), // Datadog expects seconds
|
|
214
|
+
totalTests: this.totalTests,
|
|
215
|
+
passedTests: this.passedTests,
|
|
216
|
+
failedTests: this.failedTests,
|
|
217
|
+
flakyTests: this.flakyTests,
|
|
218
|
+
skippedTests: this.skippedTests,
|
|
219
|
+
timedOutTests: this.timedOutTests,
|
|
220
|
+
durationMs: this.runEndTime - this.runStartTime,
|
|
221
|
+
browserCreationMs: this.browserCreationMs,
|
|
222
|
+
retryTimeMs: this.retryTimeMs,
|
|
223
|
+
navigationMs: this.navigationMs,
|
|
224
|
+
allPassed: this.failedTests === 0 && result.status === 'passed',
|
|
225
|
+
passRate: (0, transform_1.calculatePassRate)(this.passedTests, this.totalTests - this.skippedTests),
|
|
226
|
+
flakyRate: (0, transform_1.calculateFlakyRate)(this.flakyTests, this.totalTests - this.skippedTests),
|
|
227
|
+
workerCount: this.workerCount,
|
|
228
|
+
avgTestDurationMs,
|
|
229
|
+
};
|
|
230
|
+
// Build base tags
|
|
231
|
+
const baseTags = {
|
|
232
|
+
branch: this.options.ci?.branchName,
|
|
233
|
+
project: this.projectNames.length > 0 ? this.projectNames : undefined,
|
|
234
|
+
prenvId: this.options.prenvId,
|
|
235
|
+
triggeredBy: this.options.ci?.triggeredBy,
|
|
236
|
+
env: this.options.env,
|
|
237
|
+
service: this.options.service ?? 'playwright-tests',
|
|
238
|
+
product: this.options.product,
|
|
239
|
+
};
|
|
240
|
+
// Build metrics payload
|
|
241
|
+
const payload = (0, transform_1.buildMetricsPayload)(runMetrics, this.testMetrics, baseTags);
|
|
242
|
+
if (this.options.dryRun || this.options.debug) {
|
|
243
|
+
// eslint-disable-next-line no-console
|
|
244
|
+
console.log('\n[Datadog Reporter] Test Run Summary:');
|
|
245
|
+
// eslint-disable-next-line no-console
|
|
246
|
+
console.log(` Total: ${runMetrics.totalTests}`);
|
|
247
|
+
// eslint-disable-next-line no-console
|
|
248
|
+
console.log(` Passed: ${runMetrics.passedTests}`);
|
|
249
|
+
// eslint-disable-next-line no-console
|
|
250
|
+
console.log(` Failed: ${runMetrics.failedTests}`);
|
|
251
|
+
// eslint-disable-next-line no-console
|
|
252
|
+
console.log(` Flaky: ${runMetrics.flakyTests}`);
|
|
253
|
+
// eslint-disable-next-line no-console
|
|
254
|
+
console.log(` Skipped: ${runMetrics.skippedTests}`);
|
|
255
|
+
// eslint-disable-next-line no-console
|
|
256
|
+
console.log(` Timed Out: ${runMetrics.timedOutTests}`);
|
|
257
|
+
// eslint-disable-next-line no-console
|
|
258
|
+
console.log(` Pass Rate: ${runMetrics.passRate}%`);
|
|
259
|
+
// eslint-disable-next-line no-console
|
|
260
|
+
console.log(` Flaky Rate: ${runMetrics.flakyRate}%`);
|
|
261
|
+
// eslint-disable-next-line no-console
|
|
262
|
+
console.log(` Uptime: ${runMetrics.allPassed ? 1 : 0}`);
|
|
263
|
+
// eslint-disable-next-line no-console
|
|
264
|
+
console.log(` Duration: ${runMetrics.durationMs}ms`);
|
|
265
|
+
// eslint-disable-next-line no-console
|
|
266
|
+
console.log(` Avg Test Duration: ${runMetrics.avgTestDurationMs}ms`);
|
|
267
|
+
// eslint-disable-next-line no-console
|
|
268
|
+
console.log(` Browser Creation: ${runMetrics.browserCreationMs}ms`);
|
|
269
|
+
// eslint-disable-next-line no-console
|
|
270
|
+
console.log(` Navigation Time: ${runMetrics.navigationMs}ms`);
|
|
271
|
+
// eslint-disable-next-line no-console
|
|
272
|
+
console.log(` Workers: ${runMetrics.workerCount}`);
|
|
273
|
+
// eslint-disable-next-line no-console
|
|
274
|
+
console.log(` Metrics to send: ${payload.series.length}`);
|
|
275
|
+
}
|
|
276
|
+
if (!this.options.dryRun) {
|
|
277
|
+
await this.datadog.submitMetrics(payload);
|
|
278
|
+
await this.datadog.dispose();
|
|
279
|
+
}
|
|
280
|
+
if (this.options.debug) {
|
|
281
|
+
// eslint-disable-next-line no-console
|
|
282
|
+
console.log('[Datadog Reporter] Finished');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
// eslint-disable-next-line no-console
|
|
287
|
+
console.error("[Datadog Reporter] Could not perform 'onEnd' step", error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
exports.default = DatadogReporter;
|
|
292
|
+
/**
|
|
293
|
+
* Helper function for type-safe configuration
|
|
294
|
+
*/
|
|
295
|
+
const defineDatadogReporterConfig = (options) => options;
|
|
296
|
+
exports.defineDatadogReporterConfig = defineDatadogReporterConfig;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const test_1 = require("@playwright/test");
|
|
4
|
+
/**
|
|
5
|
+
* Browser tests to generate browser creation time and navigation metrics.
|
|
6
|
+
* These tests actually launch a browser and navigate to pages.
|
|
7
|
+
*/
|
|
8
|
+
test_1.test.describe('Browser Tests - Navigation Metrics', () => {
|
|
9
|
+
(0, test_1.test)('navigate to example.com', async ({ page }) => {
|
|
10
|
+
await page.goto('https://example.com');
|
|
11
|
+
await (0, test_1.expect)(page.locator('h1')).toContainText('Example Domain');
|
|
12
|
+
});
|
|
13
|
+
(0, test_1.test)('navigate to httpbin - GET request', async ({ page }) => {
|
|
14
|
+
await page.goto('https://httpbin.org/get');
|
|
15
|
+
await (0, test_1.expect)(page.locator('body')).toContainText('origin');
|
|
16
|
+
});
|
|
17
|
+
(0, test_1.test)('navigate and interact with elements', async ({ page }) => {
|
|
18
|
+
await page.goto('https://example.com');
|
|
19
|
+
const title = await page.title();
|
|
20
|
+
(0, test_1.expect)(title).toContain('Example');
|
|
21
|
+
await (0, test_1.expect)(page.locator('a')).toBeVisible();
|
|
22
|
+
});
|
|
23
|
+
(0, test_1.test)('multiple page navigations', async ({ page }) => {
|
|
24
|
+
await page.goto('https://example.com');
|
|
25
|
+
await (0, test_1.expect)(page.locator('h1')).toBeVisible();
|
|
26
|
+
await page.goto('https://httpbin.org/html');
|
|
27
|
+
await (0, test_1.expect)(page.locator('h1')).toBeVisible();
|
|
28
|
+
});
|
|
29
|
+
(0, test_1.test)('page with delays', async ({ page }) => {
|
|
30
|
+
await page.goto('https://httpbin.org/delay/1');
|
|
31
|
+
await (0, test_1.expect)(page.locator('body')).toContainText('origin');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
test_1.test.describe('Browser Tests - Performance', () => {
|
|
35
|
+
(0, test_1.test)('measure page load', async ({ page }) => {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
await page.goto('https://example.com');
|
|
38
|
+
const loadTime = Date.now() - startTime;
|
|
39
|
+
(0, test_1.expect)(loadTime).toBeLessThan(10000); // Should load within 10s
|
|
40
|
+
await (0, test_1.expect)(page.locator('h1')).toBeVisible();
|
|
41
|
+
});
|
|
42
|
+
(0, test_1.test)('check page content', async ({ page }) => {
|
|
43
|
+
await page.goto('https://example.com');
|
|
44
|
+
// Check multiple elements
|
|
45
|
+
await (0, test_1.expect)(page.locator('h1')).toBeVisible();
|
|
46
|
+
await (0, test_1.expect)(page.locator('p').first()).toContainText('This domain is for use in');
|
|
47
|
+
await (0, test_1.expect)(page.locator('a').first()).toBeVisible();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const test_1 = require("@playwright/test");
|
|
4
|
+
/**
|
|
5
|
+
* Sample tests to verify Datadog reporter metrics collection.
|
|
6
|
+
*
|
|
7
|
+
* This file contains:
|
|
8
|
+
* - Passing tests (to verify pass_rate, uptime)
|
|
9
|
+
* - A flaky test (to verify flaky detection)
|
|
10
|
+
* - A failing test (to verify failed_count)
|
|
11
|
+
* - A skipped test (to verify skipped_count)
|
|
12
|
+
*/
|
|
13
|
+
// Track retry attempts for flaky test simulation (used internally by test)
|
|
14
|
+
test_1.test.describe('Datadog Reporter Test Suite', () => {
|
|
15
|
+
(0, test_1.test)('should pass - basic assertion', async () => {
|
|
16
|
+
(0, test_1.expect)(1 + 1).toBe(2);
|
|
17
|
+
});
|
|
18
|
+
(0, test_1.test)('should pass - string comparison', async () => {
|
|
19
|
+
(0, test_1.expect)('hello').toContain('ell');
|
|
20
|
+
});
|
|
21
|
+
(0, test_1.test)('should pass - array check', async () => {
|
|
22
|
+
const arr = [1, 2, 3];
|
|
23
|
+
(0, test_1.expect)(arr).toHaveLength(3);
|
|
24
|
+
});
|
|
25
|
+
(0, test_1.test)('should pass - object matching', async () => {
|
|
26
|
+
const obj = { name: 'test', value: 42 };
|
|
27
|
+
(0, test_1.expect)(obj).toMatchObject({ name: 'test' });
|
|
28
|
+
});
|
|
29
|
+
(0, test_1.test)('should pass - async operation', async () => {
|
|
30
|
+
const result = await Promise.resolve('success');
|
|
31
|
+
(0, test_1.expect)(result).toBe('success');
|
|
32
|
+
});
|
|
33
|
+
// This test is flaky - fails on first attempt, passes on retry
|
|
34
|
+
// eslint-disable-next-line no-empty-pattern
|
|
35
|
+
(0, test_1.test)('flaky test - passes on retry', async ({}, testInfo) => {
|
|
36
|
+
// Fail on first attempt (retry 0), pass on subsequent attempts
|
|
37
|
+
if (testInfo.retry === 0) {
|
|
38
|
+
(0, test_1.expect)(true).toBe(false); // Will fail
|
|
39
|
+
}
|
|
40
|
+
(0, test_1.expect)(true).toBe(true); // Will pass on retry
|
|
41
|
+
});
|
|
42
|
+
// Uncomment to test failed test tracking:
|
|
43
|
+
// test('should fail - intentional failure', async () => {
|
|
44
|
+
// expect(1).toBe(2)
|
|
45
|
+
// })
|
|
46
|
+
test_1.test.skip('skipped test - should be counted', async () => {
|
|
47
|
+
(0, test_1.expect)(true).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
test_1.test.describe('Additional Passing Tests', () => {
|
|
51
|
+
(0, test_1.test)('math operations', async () => {
|
|
52
|
+
(0, test_1.expect)(10 * 5).toBe(50);
|
|
53
|
+
(0, test_1.expect)(100 / 4).toBe(25);
|
|
54
|
+
});
|
|
55
|
+
(0, test_1.test)('boolean logic', async () => {
|
|
56
|
+
(0, test_1.expect)(true && true).toBe(true);
|
|
57
|
+
(0, test_1.expect)(false || true).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
(0, test_1.test)('type checking', async () => {
|
|
60
|
+
(0, test_1.expect)(typeof 'string').toBe('string');
|
|
61
|
+
(0, test_1.expect)(typeof 123).toBe('number');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// Team ownership annotation example
|
|
65
|
+
// eslint-disable-next-line no-empty-pattern
|
|
66
|
+
(0, test_1.test)('test with team owner', async ({}, testInfo) => {
|
|
67
|
+
testInfo.annotations.push({ type: 'owner', description: 'QA-Platform' });
|
|
68
|
+
(0, test_1.expect)(true).toBe(true);
|
|
69
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type DatadogMetricsPayload, type MetricSeries, type MetricTags, type TestMetrics, type TestRunMetrics } from './report.types';
|
|
2
|
+
/**
|
|
3
|
+
* Converts MetricTags to Datadog tag format (key:value strings)
|
|
4
|
+
*/
|
|
5
|
+
export declare const formatTags: (tags: MetricTags) => string[];
|
|
6
|
+
/**
|
|
7
|
+
* Transforms test run metrics into Datadog metric series
|
|
8
|
+
*/
|
|
9
|
+
export declare const transformTestRunMetrics: (runMetrics: TestRunMetrics, baseTags: MetricTags) => MetricSeries[];
|
|
10
|
+
/**
|
|
11
|
+
* Transforms individual test metrics into Datadog metric series
|
|
12
|
+
* Used for tracking per-test duration and flakiness
|
|
13
|
+
*/
|
|
14
|
+
export declare const transformTestMetrics: (testMetrics: TestMetrics, timestamp: number, baseTags: MetricTags) => MetricSeries[];
|
|
15
|
+
/**
|
|
16
|
+
* Builds the complete Datadog metrics payload from test run data
|
|
17
|
+
*/
|
|
18
|
+
export declare const buildMetricsPayload: (runMetrics: TestRunMetrics, testMetricsList: TestMetrics[], baseTags: MetricTags) => DatadogMetricsPayload;
|
|
19
|
+
/**
|
|
20
|
+
* Calculates pass rate as a percentage
|
|
21
|
+
*/
|
|
22
|
+
export declare const calculatePassRate: (passed: number, total: number) => number;
|
|
23
|
+
/**
|
|
24
|
+
* Calculates flaky rate as a percentage
|
|
25
|
+
*/
|
|
26
|
+
export declare const calculateFlakyRate: (flaky: number, total: number) => number;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateFlakyRate = exports.calculatePassRate = exports.buildMetricsPayload = exports.transformTestMetrics = exports.transformTestRunMetrics = exports.formatTags = void 0;
|
|
4
|
+
/* eslint-disable max-statements, max-params */
|
|
5
|
+
const report_types_1 = require("./report.types");
|
|
6
|
+
/**
|
|
7
|
+
* Metric name prefix for all Playwright metrics
|
|
8
|
+
*/
|
|
9
|
+
const METRIC_PREFIX = 'playwright';
|
|
10
|
+
/**
|
|
11
|
+
* Converts MetricTags to Datadog tag format (key:value strings)
|
|
12
|
+
*/
|
|
13
|
+
const formatTags = (tags) => {
|
|
14
|
+
const formattedTags = [];
|
|
15
|
+
if (tags.branch) {
|
|
16
|
+
formattedTags.push(`branch:${tags.branch}`);
|
|
17
|
+
}
|
|
18
|
+
if (tags.project && tags.project.length > 0) {
|
|
19
|
+
tags.project.forEach((p) => formattedTags.push(`project:${p}`));
|
|
20
|
+
}
|
|
21
|
+
if (tags.prenvId) {
|
|
22
|
+
formattedTags.push(`prenv_id:${tags.prenvId}`);
|
|
23
|
+
}
|
|
24
|
+
if (tags.triggeredBy) {
|
|
25
|
+
formattedTags.push(`triggered_by:${tags.triggeredBy}`);
|
|
26
|
+
}
|
|
27
|
+
if (tags.env) {
|
|
28
|
+
formattedTags.push(`env:${tags.env}`);
|
|
29
|
+
}
|
|
30
|
+
if (tags.service) {
|
|
31
|
+
formattedTags.push(`service:${tags.service}`);
|
|
32
|
+
}
|
|
33
|
+
if (tags.product) {
|
|
34
|
+
formattedTags.push(`product:${tags.product}`);
|
|
35
|
+
}
|
|
36
|
+
return formattedTags;
|
|
37
|
+
};
|
|
38
|
+
exports.formatTags = formatTags;
|
|
39
|
+
/**
|
|
40
|
+
* Creates a metric series for a gauge value
|
|
41
|
+
*/
|
|
42
|
+
const createGauge = (name, value, timestamp, tags, unit) => ({
|
|
43
|
+
metric: `${METRIC_PREFIX}.${name}`,
|
|
44
|
+
type: report_types_1.MetricTypes.GAUGE,
|
|
45
|
+
points: [{ timestamp, value }],
|
|
46
|
+
tags,
|
|
47
|
+
...(unit && { unit }),
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Creates a metric series for a count value
|
|
51
|
+
*/
|
|
52
|
+
const createCount = (name, value, timestamp, tags) => ({
|
|
53
|
+
metric: `${METRIC_PREFIX}.${name}`,
|
|
54
|
+
type: report_types_1.MetricTypes.COUNT,
|
|
55
|
+
points: [{ timestamp, value }],
|
|
56
|
+
tags,
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Creates a metric series for a gauge value (used for distributions since API v2 doesn't support distribution type directly)
|
|
60
|
+
*/
|
|
61
|
+
const createDistribution = (name, value, timestamp, tags, unit) => ({
|
|
62
|
+
metric: `${METRIC_PREFIX}.${name}`,
|
|
63
|
+
type: report_types_1.MetricTypes.GAUGE, // Use gauge for distribution-like metrics in v2 API
|
|
64
|
+
points: [{ timestamp, value }],
|
|
65
|
+
tags,
|
|
66
|
+
...(unit && { unit }),
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Transforms test run metrics into Datadog metric series
|
|
70
|
+
*/
|
|
71
|
+
const transformTestRunMetrics = (runMetrics, baseTags) => {
|
|
72
|
+
const tags = (0, exports.formatTags)(baseTags);
|
|
73
|
+
const { timestamp } = runMetrics;
|
|
74
|
+
const series = [];
|
|
75
|
+
// Executive View Metrics
|
|
76
|
+
// Uptime: 1 if all tests passed, 0 otherwise
|
|
77
|
+
series.push(createGauge('test_run.uptime', runMetrics.allPassed ? 1 : 0, timestamp, tags));
|
|
78
|
+
// Pass rate as percentage (0-100)
|
|
79
|
+
series.push(createGauge('test_run.pass_rate', runMetrics.passRate, timestamp, tags, 'percent'));
|
|
80
|
+
// Engineer View Metrics
|
|
81
|
+
// Flaky rate as percentage (0-100)
|
|
82
|
+
series.push(createGauge('test_run.flaky_rate', runMetrics.flakyRate, timestamp, tags, 'percent'));
|
|
83
|
+
// Test counts
|
|
84
|
+
series.push(createGauge('test_run.test_count', runMetrics.totalTests, timestamp, tags));
|
|
85
|
+
series.push(createCount('test_run.failed_count', runMetrics.failedTests, timestamp, tags));
|
|
86
|
+
series.push(createCount('test_run.flaky_count', runMetrics.flakyTests, timestamp, tags));
|
|
87
|
+
series.push(createCount('test_run.skipped_count', runMetrics.skippedTests, timestamp, tags));
|
|
88
|
+
series.push(createCount('test_run.passed_count', runMetrics.passedTests, timestamp, tags));
|
|
89
|
+
// Duration metrics
|
|
90
|
+
series.push(createDistribution('test_run.duration_ms', runMetrics.durationMs, timestamp, tags, 'millisecond'));
|
|
91
|
+
// Average test duration
|
|
92
|
+
if (runMetrics.avgTestDurationMs > 0) {
|
|
93
|
+
series.push(createDistribution('test_run.avg_test_duration_ms', runMetrics.avgTestDurationMs, timestamp, tags, 'millisecond'));
|
|
94
|
+
}
|
|
95
|
+
// Browser creation time
|
|
96
|
+
if (runMetrics.browserCreationMs > 0) {
|
|
97
|
+
series.push(createDistribution('test_run.browser_creation_ms', runMetrics.browserCreationMs, timestamp, tags, 'millisecond'));
|
|
98
|
+
}
|
|
99
|
+
// Navigation time
|
|
100
|
+
if (runMetrics.navigationMs > 0) {
|
|
101
|
+
series.push(createDistribution('test_run.navigation_ms', runMetrics.navigationMs, timestamp, tags, 'millisecond'));
|
|
102
|
+
}
|
|
103
|
+
// Retry time
|
|
104
|
+
if (runMetrics.retryTimeMs > 0) {
|
|
105
|
+
series.push(createDistribution('test_run.retry_time_ms', runMetrics.retryTimeMs, timestamp, tags, 'millisecond'));
|
|
106
|
+
}
|
|
107
|
+
// Worker count
|
|
108
|
+
if (runMetrics.workerCount > 0) {
|
|
109
|
+
series.push(createGauge('test_run.worker_count', runMetrics.workerCount, timestamp, tags));
|
|
110
|
+
}
|
|
111
|
+
// Timeout count
|
|
112
|
+
if (runMetrics.timedOutTests > 0) {
|
|
113
|
+
series.push(createCount('test_run.timeout_count', runMetrics.timedOutTests, timestamp, tags));
|
|
114
|
+
}
|
|
115
|
+
return series;
|
|
116
|
+
};
|
|
117
|
+
exports.transformTestRunMetrics = transformTestRunMetrics;
|
|
118
|
+
/**
|
|
119
|
+
* Transforms individual test metrics into Datadog metric series
|
|
120
|
+
* Used for tracking per-test duration and flakiness
|
|
121
|
+
*/
|
|
122
|
+
const transformTestMetrics = (testMetrics, timestamp, baseTags) => {
|
|
123
|
+
const series = [];
|
|
124
|
+
// Build tags with test-specific info
|
|
125
|
+
const testTags = (0, exports.formatTags)(baseTags);
|
|
126
|
+
testTags.push(`test_name:${testMetrics.name.replace(/[,|]/g, '_').substring(0, 200)}`);
|
|
127
|
+
if (testMetrics.team) {
|
|
128
|
+
testTags.push(`team:${testMetrics.team}`);
|
|
129
|
+
}
|
|
130
|
+
if (testMetrics.project) {
|
|
131
|
+
testTags.push(`test_project:${testMetrics.project}`);
|
|
132
|
+
}
|
|
133
|
+
// Test duration
|
|
134
|
+
series.push(createDistribution('test.duration_ms', testMetrics.durationMs, timestamp, testTags, 'millisecond'));
|
|
135
|
+
// Track navigation time per test
|
|
136
|
+
if (testMetrics.navigationMs > 0) {
|
|
137
|
+
series.push(createDistribution('test.navigation_ms', testMetrics.navigationMs, timestamp, testTags, 'millisecond'));
|
|
138
|
+
}
|
|
139
|
+
// Track retries
|
|
140
|
+
if (testMetrics.retryCount > 0) {
|
|
141
|
+
series.push(createCount('test.retry_count', testMetrics.retryCount, timestamp, testTags));
|
|
142
|
+
}
|
|
143
|
+
// Track flaky tests
|
|
144
|
+
if (testMetrics.isFlaky) {
|
|
145
|
+
series.push(createCount('test.flaky', 1, timestamp, testTags));
|
|
146
|
+
}
|
|
147
|
+
// Track timed out tests
|
|
148
|
+
if (testMetrics.isTimedOut) {
|
|
149
|
+
series.push(createCount('test.timeout', 1, timestamp, testTags));
|
|
150
|
+
}
|
|
151
|
+
return series;
|
|
152
|
+
};
|
|
153
|
+
exports.transformTestMetrics = transformTestMetrics;
|
|
154
|
+
/**
|
|
155
|
+
* Builds the complete Datadog metrics payload from test run data
|
|
156
|
+
*/
|
|
157
|
+
const buildMetricsPayload = (runMetrics, testMetricsList, baseTags) => {
|
|
158
|
+
const series = [];
|
|
159
|
+
// Add test run level metrics
|
|
160
|
+
series.push(...(0, exports.transformTestRunMetrics)(runMetrics, baseTags));
|
|
161
|
+
// Add individual test metrics (for flaky tests and duration tracking)
|
|
162
|
+
for (const testMetrics of testMetricsList) {
|
|
163
|
+
series.push(...(0, exports.transformTestMetrics)(testMetrics, runMetrics.timestamp, baseTags));
|
|
164
|
+
}
|
|
165
|
+
return { series };
|
|
166
|
+
};
|
|
167
|
+
exports.buildMetricsPayload = buildMetricsPayload;
|
|
168
|
+
/**
|
|
169
|
+
* Calculates pass rate as a percentage
|
|
170
|
+
*/
|
|
171
|
+
const calculatePassRate = (passed, total) => {
|
|
172
|
+
if (total === 0)
|
|
173
|
+
return 100;
|
|
174
|
+
return Math.round((passed / total) * 10000) / 100; // Round to 2 decimal places
|
|
175
|
+
};
|
|
176
|
+
exports.calculatePassRate = calculatePassRate;
|
|
177
|
+
/**
|
|
178
|
+
* Calculates flaky rate as a percentage
|
|
179
|
+
*/
|
|
180
|
+
const calculateFlakyRate = (flaky, total) => {
|
|
181
|
+
if (total === 0)
|
|
182
|
+
return 0;
|
|
183
|
+
return Math.round((flaky / total) * 10000) / 100; // Round to 2 decimal places
|
|
184
|
+
};
|
|
185
|
+
exports.calculateFlakyRate = calculateFlakyRate;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import AnalyticsReporter, { defineAnalyticsReporterConfig } from './analytics/reporter';
|
|
2
|
+
import DatadogReporter, { defineDatadogReporterConfig } from './datadog/reporter';
|
|
2
3
|
import PermissionsReporter, { definePermissionsReporterConfig } from './permissions/reporter';
|
|
3
4
|
import RetryReporter, { defineRetryReporterConfig } from './retry/reporter';
|
|
4
5
|
import StepDurationReporter, { defineStepDurationReporterConfig } from './stepDuration/reporter';
|
|
5
6
|
import TimelineReporter, { defineTimelineReporterConfig } from './timeline/reporter';
|
|
6
7
|
import TranslationReporter, { defineTranslationReporterConfig } from './translation/reporter';
|
|
7
|
-
export { AnalyticsReporter, defineAnalyticsReporterConfig, RetryReporter, defineRetryReporterConfig, TimelineReporter, defineTimelineReporterConfig, StepDurationReporter, defineStepDurationReporterConfig, PermissionsReporter, definePermissionsReporterConfig, TranslationReporter, defineTranslationReporterConfig, };
|
|
8
|
+
export { AnalyticsReporter, defineAnalyticsReporterConfig, DatadogReporter, defineDatadogReporterConfig, RetryReporter, defineRetryReporterConfig, TimelineReporter, defineTimelineReporterConfig, StepDurationReporter, defineStepDurationReporterConfig, PermissionsReporter, definePermissionsReporterConfig, TranslationReporter, defineTranslationReporterConfig, };
|